diff --git a/README.md b/README.md index e6a968a..11b0365 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -spring-data-jpa-examples -======================== +# Test With Spring Course + +If you are struggling to write good automated tests for Spring web applications, you are not alone! [I have launched a video course](https://www.testwithspring.com/?utm_source=github&utm_medium=social&utm_content=spring-data-jpa&utm_campaign=test-with-spring-course-presales) that describes how you can write automated tests which embrace change and help you to save your time (and nerves). + +# Spring Data JPA Tutorial + +This repository contains the example applications of my [Spring Data JPA tutorial](http://www.petrikainulainen.net/spring-data-jpa-tutorial/). The READMEs of the examples provide more information about the application in question. diff --git a/criteria-api/.gitignore b/criteria-api/.gitignore new file mode 100644 index 0000000..02895f1 --- /dev/null +++ b/criteria-api/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.gradle +.idea +*.iml +build +h2db +target +node_modules +bower_components +build \ No newline at end of file diff --git a/tutorial-part-four/LICENSE b/criteria-api/LICENSE similarity index 88% rename from tutorial-part-four/LICENSE rename to criteria-api/LICENSE index b333aa5..642bfb3 100644 --- a/tutorial-part-four/LICENSE +++ b/criteria-api/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011 Petri Kainulainen +Copyright 2014 Petri Kainulainen Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -10,4 +10,4 @@ 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. \ No newline at end of file +limitations under the License. diff --git a/criteria-api/README.md b/criteria-api/README.md new file mode 100644 index 0000000..fb07560 --- /dev/null +++ b/criteria-api/README.md @@ -0,0 +1,83 @@ +This blog post is the example application of the following blog posts: + +* [Spring Data JPA Tutorial: Creating Database Queries With the JPA Criteria API](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-four-jpa-criteria-queries/) +* [Spring Data JPA Tutorial: Sorting]() - Not published yet + +You might also want to read the other parts of my Spring Data JPA Tutorial: + + +* [Spring Data JPA Tutorial: Getting the Required Dependencies](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-getting-the-required-dependencies/) +* [Spring Data JPA Tutorial: Configuration](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/) +* [Spring Data JPA Tutorial: CRUD](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/) +* [Spring Data JPA Tutorial: Introduction to Query Methods](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-introduction-to-query-methods/) +* [Spring Data JPA Tutorial: Creating Database Queries From Method Names](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-from-method-names/) +* [Spring Data JPA Tutorial: Creating Database Queries With the @Query Annotation](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-the-query-annotation/) +* [Spring Data JPA Tutorial: Creating Database Queries With Named Queries](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-named-queries/) +* [Spring Data JPA Tutorial: Creating Database Queries With Querydsl](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-five-querydsl/) +* [Spring Data JPA Tutorial: Auditing, Part One](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-one/) +* [Spring Data JPA Tutorial: Auditing, Part Two](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-two/) + + +**Note:** This application is still work in progress. + +Prerequisites +============= + +You need to install the following tools if you want to run this application: + +Backend +--------- + +* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* [Maven](http://maven.apache.org/) (the application is tested with Maven 3.2.1) + +Frontend +---------- + +* [Node.js](http://nodejs.org/) +* [NPM](https://www.npmjs.org/) +* [Bower](http://bower.io/) +* [Gulp](http://gulpjs.com/) + +You can install these tools by following these steps: + +1. Install Node.js by using a [downloaded binary](http://nodejs.org/download/) or a [package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). + You can also read this blog post: [How to install Node.js and NPM](http://blog.nodeknockout.com/post/65463770933/how-to-install-node-js-and-npm) + +2. Install Bower by using the following command: + + npm install -g bower + +3. Install Gulp by using the following command: + + npm install -g gulp + + +Running the Tests +================= + +You can run the unit tests by using the following command: + + mvn clean test -P dev + +You can run the integration tests by using the following command: + + mvn clean verify -P integration-test + +Running the Application +======================= + +You can run the application by using the following command: + + mvn clean jetty:run -P dev + +Credits +========= + +* Kyösti Herrala. The Gulp build script and its Maven integration are based on Kyösti's ideas. +* [Techniques for authentication in AngularJS applications](https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec) + +Known Issues +============ + +* If you refresh the login page, you aren't redirected away from it after successful login. \ No newline at end of file diff --git a/criteria-api/frontend/.bowerrc b/criteria-api/frontend/.bowerrc new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/criteria-api/frontend/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file diff --git a/criteria-api/frontend/.jshintrc b/criteria-api/frontend/.jshintrc new file mode 100644 index 0000000..f648d46 --- /dev/null +++ b/criteria-api/frontend/.jshintrc @@ -0,0 +1,33 @@ +{ + "globalstrict": true, + "browser": true, + "devel": true, + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "globals": { + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "angular": true, + "jQuery": true, + "_": true, + "$": true + } +} \ No newline at end of file diff --git a/criteria-api/frontend/app/app.js b/criteria-api/frontend/app/app.js new file mode 100644 index 0000000..3b12b71 --- /dev/null +++ b/criteria-api/frontend/app/app.js @@ -0,0 +1,98 @@ +'use strict'; + +var App = angular.module('app', [ + 'angular-logger', + 'http-auth-interceptor', + 'ngLocale', + 'ngCookies', + 'ngResource', + 'ngSanitize', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.router', + 'ui.utils', + 'angular-growl', + 'angularMoment', + 'angularUtils.directives.dirPagination', + 'spring-security-csrf-token-interceptor', + + //Partials + 'templates', + + //Account + 'app.account.config', 'app.account.directives', 'app.account.controllers', 'app.account.services', + + //Common + 'app.common.config', 'app.common.controllers', 'app.common.directives', 'app.common.services', + + //Todo + 'app.todo.controllers', 'app.todo.directives', 'app.todo.services', + + //Search + 'app.search.controllers', 'app.search.directives', 'app.search.services' + +]); + +App.run(['$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', 'authService', 'AuthenticationService', 'COMMON_EVENTS', + function ($log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser, authService, AuthenticationService, COMMON_EVENTS) { + + var logger = $log.getInstance('app'); + + //This function retries all requests that were failed because of + //the 401 response. + function listenAuthenticationEvents() { + var confirmLogin = function() { + authService.loginConfirmed(); + }; + + $rootScope.$on(AUTH_EVENTS.loginSuccess, confirmLogin); + + var viewLogInPage = function() { + logger.info('User is not authenticated. Rendering login view.'); + $state.go('todo.login'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthenticated, viewLogInPage); + + var viewTodoListPage = function() { + logger.info("User logged out. REndering todo list view."); + $state.go('todo.list', {}, {reload: true}); + }; + + $rootScope.$on(AUTH_EVENTS.logoutSuccess, viewTodoListPage); + + var viewForbiddenPage = function() { + logger.info('Permission was denied for user: %j', AuthenticatedUser); + $state.go('todo.forbidden'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthorized, viewForbiddenPage); + } + + function listenCommonEvents() { + + var view404Page = function() { + logger.info('Requested page was not found.'); + $state.go('todo.404'); + }; + + $rootScope.$on(COMMON_EVENTS.notFound, view404Page); + } + + //This function ensures that anonymous users cannot access states + //that marked as protected (i.e. the value of the authenticated + //property is set to true). + function secureProtectedStates() { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + logger.trace('Moving to state: %s', toState.name); + AuthenticationService.authorizeStateChange(event, toState, toParams); + }); + } + + $rootScope.currentUser = AuthenticatedUser; + + listenAuthenticationEvents(); + listenCommonEvents(); + secureProtectedStates(); +}]); + diff --git a/criteria-api/frontend/app/assets/i18n/en.json b/criteria-api/frontend/app/assets/i18n/en.json new file mode 100644 index 0000000..869176b --- /dev/null +++ b/criteria-api/frontend/app/assets/i18n/en.json @@ -0,0 +1,101 @@ +{ + "app.title.label": "Spring Data JPA Tutorial - Query Methods", + "dialogs": { + "delete.dialog": { + "cancel.button.label": "Cancel", + "delete.button.label": "Delete", + "text": "Are you sure that you want to delete the todo entry with title: {{title}}?", + "title": "Delete todo entry?" + } + }, + "directives": { + "login.form": { + "login.button": "Login", + "login.failed": "Login failed!" + }, + "log.out.link.label": "Log Out", + "todo.form": { + "cancel.button": "Cancel", + "save.button": "Save" + } + }, + "footer.message": "Spring Data JPA example application by Petri Kainulainen", + "header.brand.label": "Spring Data JPA Tutorial", + "pages": { + "add.page": { + "title": "Add new todo entry", + "link.label": "Add new todo entry" + }, + "delete.link": "Delete", + "edit.page": { + "link.label": "Edit", + "title": "Edit todo entry" + }, + "forbidden.page": { + "text": "Permission denied.", + "title": "Forbidden" + }, + "not.found.page": { + "text": "The page that you were looking for was not found.", + "title": "Not Found" + }, + "list.page": { + "title": "Things to do", + "texts": { + "no.todo.entries.found": "Nothing to do (yet)." + } + }, + "login.page": { + "title": "Log In" + }, + "search.results.page": { + "texts": { + "no.todo.entries.found": "No todo entries was found with the given search term." + }, + "title": "Search Results" + }, + "view.page": { + "title": "View Todo Entry" + } + }, + "login": { + "help": "Log in by using username: 'user' and password: 'password'", + "username": "Username", + "username.placeholder": "Enter username", + "password": "Password", + "password.placeholder": "Enter password" + }, + "search": { + "term.field.placeholder": "Search", + "missing.characters.text": "{{missingCharCount}} characters missing" + }, + "todo": { + "created.by.prefix": "by", + "creation.time": "Created at", + "description": "Description", + "description.placeholder": "Enter description", + "messages": { + "description.maxLength": "Description cannot be longer than 500 characters", + "title.maxLength": "Title cannot be longer than 100 characters", + "title.required": "Title is required" + }, + "modified.by.prefix": "by", + "modification.time": "Modified at", + "notifications": { + "add": { + "error": "Adding a new todo entry failed.", + "success": "A new todo entry was added." + }, + "delete": { + "error": "Deleting the todo entry failed.", + "success": "Deleted the todo entry." + }, + "edit": { + "error": "Updating the information of a todo entry failed.", + "success": "Updated the information of the todo entry." + } + }, + "title": "Title", + "title.placeholder": "Enter title" + } +} \ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/account/forbidden-view.html b/criteria-api/frontend/app/assets/partials/account/forbidden-view.html new file mode 100644 index 0000000..c761f3e --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/account/forbidden-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/account/login-form-directive.html b/criteria-api/frontend/app/assets/partials/account/login-form-directive.html new file mode 100644 index 0000000..d2f14aa --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/account/login-form-directive.html @@ -0,0 +1,36 @@ +
+ + +
+ +
+
+ : + +
+
+ : + +
+
+ +
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/account/login-view.html b/criteria-api/frontend/app/assets/partials/account/login-view.html new file mode 100644 index 0000000..199d339 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/account/login-view.html @@ -0,0 +1,6 @@ +

+ +
+
+

+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/account/logout-link-directive.html b/criteria-api/frontend/app/assets/partials/account/logout-link-directive.html new file mode 100644 index 0000000..4d9550a --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/account/logout-link-directive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/common/not-found-view.html b/criteria-api/frontend/app/assets/partials/common/not-found-view.html new file mode 100644 index 0000000..7edf553 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/common/not-found-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/search/dirPagination.tpl.html b/criteria-api/frontend/app/assets/partials/search/dirPagination.tpl.html new file mode 100644 index 0000000..558aa20 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/search/dirPagination.tpl.html @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/search/search-form-directive.html b/criteria-api/frontend/app/assets/partials/search/search-form-directive.html new file mode 100644 index 0000000..674143e --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/search/search-form-directive.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/search/search-result-list-directive.html b/criteria-api/frontend/app/assets/partials/search/search-result-list-directive.html new file mode 100644 index 0000000..c38f4f7 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/search/search-result-list-directive.html @@ -0,0 +1,19 @@ +
+
+ +
+ +
+ +
+
+
+

+
diff --git a/criteria-api/frontend/app/assets/partials/search/search-result-view.html b/criteria-api/frontend/app/assets/partials/search/search-result-view.html new file mode 100644 index 0000000..2d8cd39 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/search/search-result-view.html @@ -0,0 +1,4 @@ +
+

+
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/add-todo-view.html b/criteria-api/frontend/app/assets/partials/todo/add-todo-view.html new file mode 100644 index 0000000..0a0406a --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/add-todo-view.html @@ -0,0 +1,9 @@ +

+ +
+
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/delete-todo-modal.html b/criteria-api/frontend/app/assets/partials/todo/delete-todo-modal.html new file mode 100644 index 0000000..b390319 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/delete-todo-modal.html @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/edit-todo-view.html b/criteria-api/frontend/app/assets/partials/todo/edit-todo-view.html new file mode 100644 index 0000000..1695ae6 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/edit-todo-view.html @@ -0,0 +1,8 @@ +

+
+
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/todo-form-directive.html b/criteria-api/frontend/app/assets/partials/todo/todo-form-directive.html new file mode 100644 index 0000000..c7815d0 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/todo-form-directive.html @@ -0,0 +1,52 @@ +
+
+ : + +
+ + +
+
+
+ : + +
+ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/todo-list-directive.html b/criteria-api/frontend/app/assets/partials/todo/todo-list-directive.html new file mode 100644 index 0000000..60ed955 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/todo-list-directive.html @@ -0,0 +1,8 @@ +
+

+
+
+
+ {{todoEntry.title}} +
+
diff --git a/criteria-api/frontend/app/assets/partials/todo/todo-list-view.html b/criteria-api/frontend/app/assets/partials/todo/todo-list-view.html new file mode 100644 index 0000000..6a83ba4 --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/todo-list-view.html @@ -0,0 +1,7 @@ +
+

+ +
+ +
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/assets/partials/todo/view-todo-view.html b/criteria-api/frontend/app/assets/partials/todo/view-todo-view.html new file mode 100644 index 0000000..374c16d --- /dev/null +++ b/criteria-api/frontend/app/assets/partials/todo/view-todo-view.html @@ -0,0 +1,25 @@ +
+

+ +
+

{{todoEntry.title}}

+

{{todoEntry.description}}

+
+

+ + {{"todo.creation.time" | translate}}: {{todoEntry.creationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.created.by.prefix" | translate}} {{todoEntry.createdByUser}} + {{"todo.modification.time" | translate }}: {{todoEntry.modificationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.modified.by.prefix" | translate}} {{todoEntry.modifiedByUser}} + +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/criteria-api/frontend/app/module/account/account.config.js b/criteria-api/frontend/app/module/account/account.config.js new file mode 100644 index 0000000..c689bb1 --- /dev/null +++ b/criteria-api/frontend/app/module/account/account.config.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('app.account.config', []) + .constant('AUTH_EVENTS', { + loginSuccess: 'event:auth-login-success', + loginFailed: 'event:auth-login-failed', + logoutSuccess: 'event:auth-logout-success', + sessionTimeout: 'event:auth-session-timeout', + notAuthenticated: 'event:auth-loginRequired', + notAuthorized: 'event:auth-forbidden' + }) + .config(['csrfProvider', function(csrfProvider) { + // optional configurations + csrfProvider.config({ + httpTypes: ['PUT', 'POST', 'DELETE'], + maxRetries: 1, + url: '/api/csrf' + }); + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/account/account.controllers.js b/criteria-api/frontend/app/module/account/account.controllers.js new file mode 100644 index 0000000..79f6fbe --- /dev/null +++ b/criteria-api/frontend/app/module/account/account.controllers.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('app.account.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.login', { + url: 'login', + controller: 'LoginController', + templateUrl: 'account/login-view.html' + }) + .state('todo.forbidden', { + url: 'forbidden', + controller: 'ForbiddenController', + templateUrl: 'account/forbidden-view.html' + }); + } + ]) + .controller('ForbiddenController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.ForbiddenController'); + logger.info("Rendering forbidden view."); + }]) + .controller('LoginController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.LoginController'); + logger.info('Rendering login form.'); + }]); + diff --git a/criteria-api/frontend/app/module/account/account.directives.js b/criteria-api/frontend/app/module/account/account.directives.js new file mode 100644 index 0000000..2ff0aa6 --- /dev/null +++ b/criteria-api/frontend/app/module/account/account.directives.js @@ -0,0 +1,44 @@ +'use strict'; + +angular.module('app.account.directives', []) + .directive('logOutLink', ['$log', 'AuthenticationService', function ($log, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.logOutLink'); + + return { + link: function (scope, element, attr) { + scope.logOut = function() { + logger.info('Logging user out.'); + AuthenticationService.logOut(); + }; + }, + templateUrl: 'account/logout-link-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('loginForm', ['$log', 'AUTH_EVENTS', 'AuthenticationService', function ($log, AUTH_EVENTS, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.loginForm'); + + return { + link: function (scope, element, attr) { + scope.login = {}; + scope.loginFailed = false; + + scope.$on(AUTH_EVENTS.loginFailed, function() { + logger.info('Received login failed event.'); + scope.loginFailed = true; + }); + + scope.submitLoginForm = function() { + logger.info('Submitting log in form.'); + AuthenticationService.logIn(scope.login.username, scope.login.password); + }; + }, + templateUrl: 'account/login-form-directive.html', + scope: { + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/account/account.services.js b/criteria-api/frontend/app/module/account/account.services.js new file mode 100644 index 0000000..7cbd82d --- /dev/null +++ b/criteria-api/frontend/app/module/account/account.services.js @@ -0,0 +1,79 @@ +'use strict'; + +angular.module('app.account.services', ['ngResource']) + .service('AuthenticatedUser', function () { + this.create = function (username, role) { + this.username = username; + this.role = role; + }; + this.destroy = function () { + this.username = null; + this.role = null; + }; + }) + .factory('AuthenticationService', ['$http', '$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', + function($http, $log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser) { + + var logger = $log.getInstance('app.account.services.AuthenticationService'); + + return { + authorizeStateChange: function(event, toState, toParams) { + logger.debug('Authorizing state change to state: %s', toState.name); + if (toState.authenticate && !this.isAuthenticated()) { + event.preventDefault(); + + logger.debug('Authentication is not found. Fetching it from the backend.'); + var self = this; + $http.get('/api/authenticated-user').success(function(user) { + logger.debug('Found authenticated user: %j', user); + AuthenticatedUser.create(user.username, user.role); + + if (!self.isAuthenticated) { + logger.debug('Unauthenticated users is: %j', AuthenticatedUser); + $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); + } + else { + logger.debug('User is authenticated. Continuing to the target state: %s', toState.name); + $state.go(toState.name, toParams); + } + }); + } + }, + isAuthenticated: function() { + logger.debug('Checking if user: %j is authenticated.', AuthenticatedUser); + return AuthenticatedUser.username; + }, + logIn: function(username, password) { + logger.info('Logging in user with username: %s', username); + + var transform = function(data){ + return $.param(data); + }; + + $http.post('/api/login', {username: username, password: password}, { + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + ignoreAuthModule: true, + transformRequest: transform + }) + .success(function(user) { + logger.info('Login successful for user: %j', user); + AuthenticatedUser.create(user.username, user.role); + $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); + }) + .error(function() { + logger.info('Login failed'); + $rootScope.$broadcast(AUTH_EVENTS.loginFailed); + }); + }, + logOut: function() { + if (this.isAuthenticated()) { + $http.post('/api/logout', {}) + .success(function() { + logger.info('User is logged out.'); + AuthenticatedUser.destroy(); + $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); + }); + } + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/common/common.config.js b/criteria-api/frontend/app/module/common/common.config.js new file mode 100644 index 0000000..6ffca02 --- /dev/null +++ b/criteria-api/frontend/app/module/common/common.config.js @@ -0,0 +1,60 @@ +'use strict'; + +angular.module('app.common.config', []) + .constant('COMMON_EVENTS', { + notFound: 'event:not-found' + }) + .config(['logEnhancerProvider', function (logEnhancerProvider) { + logEnhancerProvider.datetimePattern = 'DD.MM.YYYY HH:mm:ss'; + logEnhancerProvider.prefixPattern = '%s::[%s]> '; + logEnhancerProvider.logLevels = { + '*': logEnhancerProvider.LEVEL.OFF + }; + }]) + .config(['$urlRouterProvider', '$locationProvider', + function ($urlRouterProvider, $locationProvider) { + //this prevents infinite $digest loop when we invoke the + //preventDefault() method in $stateChangeStart event handler. + //See: https://github.com/angular-ui/ui-router/issues/600#issuecomment-47228922 + $urlRouterProvider.otherwise( function($injector, $location) { + var $state = $injector.get("$state"); + $state.go("todo.list"); + }); + + // Without server side support html5 must be disabled. + $locationProvider.html5Mode(false); + } + ]) + .config(['$translateProvider', function ($translateProvider) { + // Initialize angular-translate + $translateProvider.useStaticFilesLoader({ + prefix: '/i18n/', + suffix: '.json' + }); + + $translateProvider.preferredLanguage('en'); + $translateProvider.useSanitizeValueStrategy('escaped'); + $translateProvider.useLocalStorage(); + $translateProvider.useMissingTranslationHandlerLog(); + }]) + .config(['growlProvider', function (growlProvider) { + growlProvider.globalTimeToLive(5000); + }]) + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push([ + '$injector', + function ($injector) { + return $injector.get('404Interceptor'); + } + ]); + }]) + .factory('404Interceptor', ['$rootScope', '$q', 'COMMON_EVENTS', function ($rootScope, $q, COMMON_EVENTS) { + return { + responseError: function(response) { + if (response.status === 404) { + $rootScope.$broadcast(COMMON_EVENTS.notFound); + } + return $q.reject(response); + } + }; + }]); diff --git a/criteria-api/frontend/app/module/common/common.controllers.js b/criteria-api/frontend/app/module/common/common.controllers.js new file mode 100644 index 0000000..811f15e --- /dev/null +++ b/criteria-api/frontend/app/module/common/common.controllers.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('app.common.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.404', { + url: 'not-found', + controller: 'NotFoundController', + templateUrl: 'common/not-found-view.html' + }); + } + ]) + .controller('NotFoundController', ['$log', function($log) { + var logger = $log.getInstance('app.common.controllers.NotFoundController'); + logger.info("Rendering 404 view."); + }]); + diff --git a/criteria-api/frontend/app/module/common/common.directives.js b/criteria-api/frontend/app/module/common/common.directives.js new file mode 100644 index 0000000..7c56027 --- /dev/null +++ b/criteria-api/frontend/app/module/common/common.directives.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('app.common.directives', []) + .directive('staticInclude', ['$http', '$templateCache', '$compile', function ($http, $templateCache, $compile) { + return function(scope, element, attrs) { + var templatePath = attrs.staticInclude; + + $http.get(templatePath, {cache: $templateCache}).success(function (response) { + var contents = $('
').html(response).contents(); + element.html(contents); + $compile(contents)(scope); + }); + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/common/common.services.js b/criteria-api/frontend/app/module/common/common.services.js new file mode 100644 index 0000000..de9d0e6 --- /dev/null +++ b/criteria-api/frontend/app/module/common/common.services.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('app.common.services', []) + .service('NotificationService', ['$rootScope', 'growl', function ($rootScope, growl) { + var flashMessageQueue = []; + + function displayNotification(message, type) { + if (type === 'success') { + growl.success(message); + } else if (type === 'warn') { + growl.warning(message); + } else if (type === 'info') { + growl.info(message); + } else { + growl.error(message); + } + } + + // Display all flash notifications after state has changed + $rootScope.$on("$stateChangeSuccess", function () { + while (flashMessageQueue.length > 0) { + var item = flashMessageQueue.shift(); + if (item) { + displayNotification(item.message, item.type); + } + } + }); + + // Public API + return { + 'flashMessage': function (message, type) { + flashMessageQueue.push({message: message, type: type || 'info'}); + } + }; + }]); diff --git a/criteria-api/frontend/app/module/search/search.controllers.js b/criteria-api/frontend/app/module/search/search.controllers.js new file mode 100644 index 0000000..6fb86a5 --- /dev/null +++ b/criteria-api/frontend/app/module/search/search.controllers.js @@ -0,0 +1,41 @@ +'use strict'; + +angular.module('app.search.controllers', []) + .constant('paginationConfig', { + firstPageNumber: 1, + pageSize: 5 + }) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.search', { + authenticate: true, + url: 'todo/search/:searchTerm/page/:pageNumber/size/:pageSize', + controller: 'SearchResultController', + templateUrl: 'search/search-result-view.html', + resolve: { + searchResults: ['TodoSearchService', '$stateParams', function(TodoSearchService, $stateParams) { + if ($stateParams.searchTerm) { + return TodoSearchService.findBySearchTerm($stateParams.searchTerm, + $stateParams.pageNumber - 1, + $stateParams.pageSize + ); + } + + return null; + }], + searchTerm: ['$stateParams', function($stateParams) { + return $stateParams.searchTerm; + }] + } + }); + } + ]) + .controller('SearchResultController', ['$log', '$scope', '$state', 'paginationConfig', 'searchResults', 'searchTerm', + function($log, $scope, $state, paginationConfig, searchResults, searchTerm) { + var logger = $log.getInstance('app.search.controllers.SearchResultController'); + logger.info('Rendering search results page for search term: %s with search results: %j', searchTerm, searchResults); + $scope.searchResults = searchResults; + $scope.searchTerm = searchTerm; + }]); + diff --git a/criteria-api/frontend/app/module/search/search.directives.js b/criteria-api/frontend/app/module/search/search.directives.js new file mode 100644 index 0000000..ecefb9c --- /dev/null +++ b/criteria-api/frontend/app/module/search/search.directives.js @@ -0,0 +1,104 @@ +'use strict'; + +angular.module('app.search.directives', []) + .directive('searchForm', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + + var logger = $log.getInstance('app.search.directives.searchForm'); + + return { + link: function (scope, element, attr) { + var userWritingSearchTerm = false; + var minimumSearchTermLength = 3; + + scope.translationData = { + missingCharCount: minimumSearchTermLength + }; + + scope.search = {}; + scope.search.searchTerm = ""; + + scope.searchFieldBlur = function() { + userWritingSearchTerm = false; + scope.search.searchTerm = ""; + scope.translationData.missingCharCount = minimumSearchTermLength; + }; + + scope.searchFieldFocus = function() { + userWritingSearchTerm = true; + }; + + scope.showMissingCharacterText = function() { + if (!scope.search.searchTerm) { + scope.search.searchTerm = ""; + } + + if (userWritingSearchTerm) { + if (scope.search.searchTerm.length < minimumSearchTermLength) { + return true; + } + } + + return false; + }; + + scope.search = function() { + logger.trace('User is using the search term: %s', scope.search.searchTerm); + + if (scope.search.searchTerm.length < minimumSearchTermLength) { + scope.translationData.missingCharCount = minimumSearchTermLength - scope.search.searchTerm.length; + logger.trace('%s characters are missing. Search is not invoked.', scope.translationData.missingCharCount); + } + else { + scope.translationData.missingCharCount = 0; + $state.go('todo.search', + { + searchTerm: scope.search.searchTerm, + pageNumber: paginationConfig.firstPageNumber, + pageSize: paginationConfig.pageSize + }, + {reload: true, inherit: true, notify: true} + ); + } + }; + + }, + templateUrl: 'search/search-form-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('searchResultList', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + var logger = $log.getInstance('app.search.directives.searchResultList'); + + return { + link: function(scope, element, attr) { + logger.debug("Rendering search result list for search term: %s and search results: %j", scope.searchTerm, scope.searchResults); + scope.todoEntries = scope.searchResults.content; + + scope.pagination = { + currentPage: scope.searchResults.number + 1, + itemsPerPage: paginationConfig.pageSize, + totalItems: scope.searchResults.totalElements + }; + + scope.pageChanged = function(newPageNumber) { + logger.debug('Requesting a new page: %s for search term: %s with page size: %s', + newPageNumber, + scope.searchTerm, + paginationConfig.pageSize + ); + + $state.go('todo.search', + {searchTerm: scope.searchTerm, pageNumber: newPageNumber, pageSize: paginationConfig.pageSize}, + {reload: true, inherit: true, notify: true} + ); + }; + }, + templateUrl: 'search/search-result-list-directive.html', + scope: { + searchResults: '=', + searchTerm: '@' + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/search/search.services.js b/criteria-api/frontend/app/module/search/search.services.js new file mode 100644 index 0000000..e007227 --- /dev/null +++ b/criteria-api/frontend/app/module/search/search.services.js @@ -0,0 +1,22 @@ +'use strict'; + +angular.module('app.search.services', ['ngResource']) + .factory('TodoSearchService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/search', {}, { + 'query': {method:'GET', isArray:false} + }); + + var logger = $log.getInstance('app.search.services.TodoSearchService'); + + return { + findBySearchTerm: function(searchTerm, pageNumber, pageSize) { + logger.info('Searching todo entries with search term: %s, pageNumber: %s, and page size: %s', searchTerm, pageNumber, pageSize); + return api.query({ + page: pageNumber, + searchTerm: searchTerm, + size: pageSize, + sort: "title" + }).$promise; + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/todo/todo.controllers.js b/criteria-api/frontend/app/module/todo/todo.controllers.js new file mode 100644 index 0000000..d4da7bf --- /dev/null +++ b/criteria-api/frontend/app/module/todo/todo.controllers.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('app.todo.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo', { + url: '/', + abstract: true, + template: '' + }) + .state('todo.add', { + authenticate: true, + url: 'todo/add', + controller: 'AddTodoController', + templateUrl: 'todo/add-todo-view.html' + }) + .state('todo.edit', { + authenticate: true, + url: 'todo/:id/edit', + controller: 'EditTodoController', + templateUrl: 'todo/edit-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }) + .state('todo.list', { + authenticate: true, + url: '', + controller: 'TodoListController', + templateUrl: 'todo/todo-list-view.html', + resolve: { + todoEntries: ['TodoService', function(TodoService) { + return TodoService.findAll(); + }] + } + }) + .state('todo.view', { + authenticate: true, + url: 'todo/:id', + controller: 'ViewTodoController', + templateUrl: 'todo/view-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }); + } + ]) + .controller('AddTodoController', ['$log', '$scope', function($log, $scope) { + var logger = $log.getInstance('app.todo.controllers.AddTodoController'); + logger.info('Rendering add todo entry page.'); + $scope.todoEntry = {}; + }]) + .controller('EditTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.EditTodoController'); + logger.info('Rendering edit todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]) + .controller('TodoListController', ['$log', '$scope', 'todoEntries', function($log, $scope, todoEntries) { + var logger = $log.getInstance('app.todo.controllers.TodoListController'); + logger.info('Rendering todo entry list page for %s todo entries.', todoEntries.length); + $scope.todoEntries = todoEntries; + }]) + .controller('ViewTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.ViewTodoController'); + logger.info('Rendering view todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/todo/todo.directives.js b/criteria-api/frontend/app/module/todo/todo.directives.js new file mode 100644 index 0000000..a2377b5 --- /dev/null +++ b/criteria-api/frontend/app/module/todo/todo.directives.js @@ -0,0 +1,102 @@ +'use strict'; + +angular.module('app.todo.directives', []) + .controller('DeleteTodoController', ['$log', '$scope', '$modalInstance', '$state', 'TodoService', 'todoEntry', 'successCallback', 'errorCallback', + function($log, $scope, $modalInstance, $state, TodoService, todoEntry, successCallback, errorCallback) { + var logger = $log.getInstance('app.todo.directives.DeleteTodoController'); + + logger.info('Showing delete confirmation dialog for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + + $scope.cancel = function() { + logger.info('User clicked cancel button. Todo entry is not deleted.'); + $modalInstance.dismiss('cancel'); + }; + + $scope.delete = function() { + logger.info('User clicked delete button. Todo entry is deleted.'); + $modalInstance.close(); + TodoService.delete(todoEntry, successCallback, errorCallback); + }; + }]) + .directive('deleteTodoEntryButton', ['$modal', '$state', 'NotificationService', function($modal, $state, NotificationService) { + return { + link: function (scope, element, attr) { + scope.onSuccess = function() { + NotificationService.flashMessage('todo.notifications.delete.success', 'success'); + $state.go('todo.list'); + }; + + scope.onError = function() { + NotificationService.flashMessage('todo.notifications.delete.error', 'error'); + }; + + scope.showDeleteConfirmationDialog = function() { + $modal.open({ + templateUrl: 'todo/delete-todo-modal.html', + controller: 'DeleteTodoController', + resolve: { + errorCallback: function() { + return scope.onError; + }, + successCallback: function() { + return scope.onSuccess; + }, + todoEntry: function () { + return scope.todoEntry; + } + } + }); + }; + }, + template: '', + scope: { + todoEntry: '=' + } + }; + }]) + .directive('todoEntryForm', ['$log', '$state', 'NotificationService', 'TodoService', function($log, $state, NotificationService, TodoService) { + var logger = $log.getInstance('app.todo.directives.todoEntryForm'); + + return { + link: function (scope, element, attr) { + scope.saveTodoEntry = function() { + logger.info('Saving todo entry: %j', scope.todoEntry); + + var onSuccess = function(saved) { + NotificationService.flashMessage(scope.successMessageKey, 'success'); + $state.go('todo.view', {id: saved.id}); + }; + + var onError = function() { + NotificationService.flashMessage(scope.errorMessageKey, 'errors'); + }; + + if (scope.formType === 'add') { + TodoService.add(scope.todoEntry, onSuccess, onError); + } + else if (scope.formType === 'edit') { + TodoService.update(scope.todoEntry, onSuccess, onError); + } + else { + logger.error('Unknown form type: %s', scope.formType); + } + }; + }, + templateUrl: 'todo/todo-form-directive.html', + scope: { + errorMessageKey: '@', + formType: '@', + todoEntry: '=', + successMessageKey: '@' + } + }; + }]) + .directive('todoEntryList', [function() { + return { + templateUrl: 'todo/todo-list-directive.html', + scope: { + todoEntries: '=' + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/module/todo/todo.services.js b/criteria-api/frontend/app/module/todo/todo.services.js new file mode 100644 index 0000000..f49f622 --- /dev/null +++ b/criteria-api/frontend/app/module/todo/todo.services.js @@ -0,0 +1,61 @@ +'use strict'; + +angular.module('app.todo.services', ['ngResource']) + .factory('TodoService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/:id', {"id": "@id"}, { + get: {method: 'GET'}, + save: {method: 'POST'}, + update: {method: 'PUT'}, + query: {method: 'GET', params: {}, isArray: true} + }); + + var logger = $log.getInstance('app.todo.services.TodoService'); + + return { + add: function(todo, successCallback, errorCallback) { + logger.info('Adding new todo entry: %j', todo); + return api.save(todo, + function(added) { + logger.info('Added a new todo entry: %j', added); + successCallback(added); + }, + function(error) { + logger.error('Adding a todo entry failed because of an error: %j', error); + errorCallback(error); + }); + }, + delete: function(todo, successCallback, errorCallback) { + logger.info('Deleting todo entry: %j', todo); + return api.delete(todo, + function(deleted) { + logger.info('Deleted todo entry: %j', deleted); + successCallback(deleted); + }, + function(error) { + logger.error('Deleting the todo entry failed because of an error: %j', error); + errorCallback(error); + } + ); + }, + findAll: function() { + logger.info('Finding all todo entries.'); + return api.query(); + }, + findById: function(id) { + logger.info('Finding todo entry by id: %s', id); + return api.get({id: id}).$promise; + }, + update: function(todo, successCallback, errorCallback) { + logger.info('Updating todo entry: %j', todo); + return api.update(todo, + function(updated) { + logger.info('Updated the information of the todo entry: %j', updated); + successCallback(updated); + }, + function(error) { + logger.error('Updating the information of the todo entry failed because of an error: %j', error); + errorCallback(error); + }); + } + }; + }]); \ No newline at end of file diff --git a/criteria-api/frontend/app/styles/app.less b/criteria-api/frontend/app/styles/app.less new file mode 100644 index 0000000..4e70998 --- /dev/null +++ b/criteria-api/frontend/app/styles/app.less @@ -0,0 +1,74 @@ +[ng-cloak] { + display: none; +} + +@import "https://github.com/bower_components/bootstrap/less/bootstrap.less"; + +// Red asterisk for required labels +label.required:before{ + content:"* "; + color:red; +} + +// styles for custom input validation +input.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +input.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +textarea.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +small.ng-error { + color: #a94442; +} + +a:hover { + cursor: pointer; +} + +.striped-list { + > .row:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + +.striped-list .row { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; +} + +.action-buttons { + text-align: right; +} diff --git a/criteria-api/frontend/bower.json b/criteria-api/frontend/bower.json new file mode 100644 index 0000000..44d3bb6 --- /dev/null +++ b/criteria-api/frontend/bower.json @@ -0,0 +1,40 @@ +{ + "name": "Spring Data JPA Tutorial - Query Methods", + "version": "0.0.1", + "main": "_public/frontend/js/app.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "console-polyfill": "~0.2.1", + "lodash": "~3.8.0", + "moment": "2.10.6", + "jquery": "2.1.0", + "bootstrap": "~3.3.4", + "angular": "~1.3.15", + "angular-http-auth": "1.2.2", + "angular-i18n": "~1.3.15", + "angular-moment": "0.10.1", + "angular-logger": "1.0.1", + "angular-sanitize": "~1.3.15", + "angular-resource": "~1.3.15", + "angular-cookies": "~1.3.15", + "angular-loader": "~1.3.15", + "angular-mocks": "~1.3.15", + "angular-translate": "~2.7.0", + "angular-translate-storage-local": "~2.7.0", + "angular-translate-loader-static-files": "~2.7.0", + "angular-translate-handler-log": "~2.7.0", + "angular-ui-utils": "~0.2.3", + "angular-ui-router": "~0.2.15", + "angular-bootstrap": "~0.13.0", + "angular-growl-v2": "0.7.3", + "angular-utils-pagination": "0.8.2", + "es5-shim": "~4.1.1", + "json3": "~3.3.2", + "script.js": "~2.5.7", + "sprintf": "1.0.3" + } +} diff --git a/criteria-api/frontend/build.config.js b/criteria-api/frontend/build.config.js new file mode 100644 index 0000000..0f85e23 --- /dev/null +++ b/criteria-api/frontend/build.config.js @@ -0,0 +1,77 @@ +'use strict'; + +var path = require('path'); + +var targetBase = './build/'; + +module.exports = { + //Configures the directories in which the files created by Gulp are copied. + target: { + js: targetBase + '/js', + lib: path.join(targetBase, 'js', 'lib'), + css: path.join(targetBase, 'css'), + partials: path.join(targetBase, 'partials'), + assets: targetBase + }, + + //Configures the location of the used libraries and frameworks. + vendorFiles: { + code: [ + './bower_components/console-polyfill/index.js', + './bower_components/lodash/dist/lodash.min.js', + './bower_components/jquery/dist/jquery.min.js', + './bower_components/angular/angular.js', + './bower_components/moment/min/moment-with-locales.min.js', + './bower_components/sprintf/dist/sprintf.min.js', + './bower_components/angular-http-auth/src/http-auth-interceptor.js', + './bower_components/angular-i18n/angular-locale_fi-fi.js', + './bower_components/angular-cookies/angular-cookies.min.js', + './bower_components/angular-moment/angular-moment.min.js', + './bower_components/angular-logger/dist/angular-logger.min.js', + './bower_components/angular-resource/angular-resource.min.js', + './bower_components/angular-sanitize/angular-sanitize.min.js', + './bower_components/angular-translate/angular-translate.min.js', + './bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', + './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js', + './bower_components/angular-translate-storage-local/angular-translate-storage-local.min.js', + './bower_components/angular-translate-handler-log/angular-translate-handler-log.min.js', + './bower_components/angular-ui-router/release/angular-ui-router.min.js', + './bower_components/angular-ui-utils/ui-utils.min.js', + './bower_components/angular-ui-utils/ui-utils-ieshiv.min.js', + './bower_components/angular-utils-pagination/dirPagination.js', + './bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', + './bower_components/angular-growl-v2/build/angular-growl.min.js', + './vendor/spring-security-csrf-token-interceptor/src/spring-security-csrf-token-interceptor.js' + ] + }, + + //Configures the location of our application's files. + appFiles: { + //Configures the location of the Javascript files. + code: [ + "./app/**/*.js" + ], + //Configures the location of the LESS files. + styleBase: "./app/styles/", + style: [ + "./bower_components/angular-growl-v2/build/angular-growl.min.css", + "./app/styles/app.less" + ], + //Configures the location of the view templates. + partials: [ + "./app/assets/partials/**/*.html" + ], + //Configures the location of static assets such as images, fonts, and localization files. + assetsBase: './app/assets/', + assets: [ + './app/assets/**' + ], + //Configures the location of shims (libraries that bring new APIs to older browsers) + shim: [ + './bower_components/angular-loader/angular-loader.min.js', + './bower_components/script.js/dist/script.min.js', + './bower_components/es5-shim/es5-shim.min.js', + './bower_components/json3/lib/json3.min.js' + ] + } +}; diff --git a/criteria-api/frontend/gulpfile.js b/criteria-api/frontend/gulpfile.js new file mode 100644 index 0000000..40f10f7 --- /dev/null +++ b/criteria-api/frontend/gulpfile.js @@ -0,0 +1,124 @@ +var gulp = require("gulp"); +var plugins = require('gulp-load-plugins')(); +var config = require('./build.config.js'); + +//Analyzes the Javascript files of our application by using JSHint and reports the found problems. +gulp.task('jshint', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.changed(config.target.js)) + .pipe(plugins.jshint('.jshintrc')) + .pipe(plugins.jshint.reporter('jshint-stylish')); +}); + +//Processes the Javascript files of our application. +gulp.task('appCode', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.sourcemaps.init()) + //Combines the Javascript files into a single Javascript file + .pipe(plugins.concat('app.min.js')) + //Minifies the created Javascript file + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.sourcemaps.write()) + //Copies the minified Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file. + .pipe(plugins.size({title: 'application'})) +}); + +//Processes the HTML templates of our application. +gulp.task('appPartials', function () { + return gulp.src(config.appFiles.partials) + .pipe(plugins.changed(config.target.js)) + //Minifies the HTML files + .pipe(plugins.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + //Loads the HTML templates into AngularJS $templateCache + .pipe(plugins.angularTemplatecache('partials.js', { + standalone: true + })) + //Copy the created Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of created Javascript file + .pipe(plugins.size({showFiles: true})) +}); + +//Processes the LESS files of our application. +gulp.task('appLess', function () { + return gulp.src(config.appFiles.style) + //Creates the final CSS file + .pipe(plugins.less({ + paths: [config.appFiles.styleBase] + })) + .pipe(plugins.concat('app.css')) + //Minifies the created CSS file + .pipe(plugins.minifyCss()) + //Copies the CSS File into the target directory + .pipe(gulp.dest(config.target.css)) + //Reports the size of the final CSS file. + .pipe(plugins.size({ title: 'css' })) +}); + +gulp.task('appAssets', function () { + return gulp.src(config.appFiles.assets, {base: config.appFiles.assetsBase}) + .pipe(gulp.dest(config.target.assets)) +}); + +//Minimizes the shims used by our application and copies them to the target directory. +gulp.task('appShim', function () { + return gulp.src(config.appFiles.shim) + .pipe(plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + })) + .pipe(gulp.dest(config.target.lib)); +}); + +//Processes the Javascript files of the libraries and frameworks that are used in our application +gulp.task('vendorCode', function () { + return gulp.src(config.vendorFiles.code) + //Combine the Javascript files into a single Javascript file + .pipe(plugins.concat('vendor.min.js')) + //Skips minification of files that are already minified. + .pipe(plugins.if('*.min.js', plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + }))) + //Minifies Javascript files that are not minified. + .pipe(plugins.if('vendor/**/*.js', plugins.uglify({ + mangle: false, + compress: true + }))) + //Copies the created file to the target directory. + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file + .pipe(plugins.size({title: 'vendor'})) +}); + +//Analyzes our Javascript files by using JSHint and invokes the build when the watched files are changed +gulp.task('watch', ['jshint', 'build'], function () { + gulp.watch(config.appFiles.partials, ['appPartials']); + gulp.watch(config.appFiles.code, ['appCode', 'jshint']); + gulp.watch(config.appFiles.style, ['appLess']); + gulp.watch(config.appFiles.assets, ['appAssets']); + gulp.watch(config.vendorFiles.code, ['vendorCode']); +}); + +//Configures the tasks of our build +gulp.task('build', [ + 'appLess', + 'appShim', + 'appAssets', + 'appPartials', + 'appCode', + 'vendorCode' +]); + +//Runs the watch task if no task is specified when gulp is run +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/criteria-api/frontend/package.json b/criteria-api/frontend/package.json new file mode 100644 index 0000000..55dfccd --- /dev/null +++ b/criteria-api/frontend/package.json @@ -0,0 +1,38 @@ +{ + "author": "Petri Kainulainen", + "name": "spring-data-jpa-tutorial-query-methods", + "description": "Angular frontend for a Spring Data JPA example.", + "version": "1.0.0", + "homepage": "", + "repository": { + "type": "git", + "url": "" + }, + "dependencies": { + "bower": "~1.4.1", + "gulp": "~3.8.11", + "gulp-angular-templatecache": "~1.6.0", + "gulp-changed": "~1.2.1", + "gulp-concat": "~2.5.2", + "gulp-if": "~1.2.5", + "gulp-insert": "^0.4.0", + "gulp-jshint": "~1.10.0", + "gulp-less": "~3.0.3", + "gulp-load-plugins": "~0.10.0", + "gulp-minify-css": "~1.1.1", + "gulp-minify-html": "~1.0.2", + "gulp-rename": "~1.2.2", + "gulp-size": "~1.2.1", + "gulp-sourcemaps": "~1.5.2", + "gulp-uglify": "~1.2.0", + "jshint-stylish": "~1.0.2" + }, + "engines": { + "node": ">=0.12.0" + } +} + + + + + diff --git a/criteria-api/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js b/criteria-api/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js new file mode 100644 index 0000000..84318da --- /dev/null +++ b/criteria-api/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("spring-security-csrf-token-interceptor",[]).factory("csrfInterceptor",["$injector","$q",function($injector){var $q=$injector.get("$q"),csrf=$injector.get("csrf"),csrfService=csrf.init();return{request:function(config){return csrfService.settings.httpTypes.indexOf(config.method.toUpperCase())>-1&&(config.headers[csrfService.settings.csrfTokenHeader]=csrfService.token),config||$q.when(config)},responseError:function(response){var $http,newToken=response.headers(csrfService.settings.csrfTokenHeader);return 403===response.status&&csrfService.numRetries -1) { + config.headers[csrfService.settings.csrfTokenHeader] = csrfService.token; + } + return config || $q.when(config); + }, + responseError: function(response) { + var $http, + newToken = response.headers(csrfService.settings.csrfTokenHeader); + + if (response.status === 403 && csrfService.numRetries < csrfService.settings.maxRetries) { + csrfService.getTokenData(); + $http = $injector.get('$http'); + csrfService.numRetries = csrfService.numRetries + 1; + return $http(response.config); + } else if (newToken) { + // update the csrf token in-case of response errors other than 403 + csrfService.token = newToken; + } + // Fix for interceptor causing failing requests + return $q.reject(response); + }, + response: function(response) { + // reset number of retries on a successful response + csrfService.numRetries = 0; + return response; + } + }; + } + ]).factory('csrfService', [ + + function() { + var defaults = { + url: '/', // the URL to which the CSRF call has to be made to get the token + csrfHttpType: 'head', // the HTTP method type which is used for making the CSRF token call + maxRetries: 5, // number of retires allowed for forbidden requests + csrfTokenHeader: 'X-CSRF-TOKEN', + httpTypes: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] // default allowed HTTP types + }; + return { + inited: false, + settings: null, + numRetries: 0, + token: '', + init: function(options) { + this.settings = angular.extend({}, defaults, options); + this.getTokenData(); + console.log(this.settings, this.defaults, options); + }, + getTokenData: function() { + var xhr = new XMLHttpRequest(); + xhr.open(this.settings.csrfHttpType, this.settings.url, false); + xhr.send(); + + this.token = xhr.getResponseHeader(this.settings.csrfTokenHeader); + this.inited = true; + } + }; + + } + ]).provider('csrf', [ + + function() { + var CsrfModel = function CsrfModel(options) { + return { + options: options, + csrfService: null + }; + }; + + return { + $get: ['csrfService', + function(csrfService) { + var self = this; + return { + init: function() { + self.model = new CsrfModel(self.options); + self.model.csrfService = csrfService; + self.model.csrfService.init(self.model.options); + return self.model.csrfService; + } + }; + } + ], + + model: null, + + options: {}, + + config: function(options) { + this.options = options; + } + }; + } + ]).config(['$httpProvider', + function($httpProvider) { + $httpProvider.interceptors.push('csrfInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/criteria-api/pom.xml b/criteria-api/pom.xml new file mode 100644 index 0000000..7206aa4 --- /dev/null +++ b/criteria-api/pom.xml @@ -0,0 +1,402 @@ + + 4.0.0 + net.petrikainulainen.springdata.jpa + criteria-api + 0.1 + Spring Data JPA - Criteria API + war + + This example demonstrates how you can create dynamic queries by using + the criteria API. + + + + + + io.spring.platform + platform-bom + 1.1.2.RELEASE + pom + import + + + + + + 1.8 + UTF-8 + true + false + 4.0.1.RELEASE + + + + + dev + + + integration-test + + false + true + + + + + + + + org.apache.commons + commons-lang3 + + + + org.slf4j + slf4j-api + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + + com.h2database + h2 + + + + com.zaxxer + HikariCP + + + + org.hibernate + hibernate-entitymanager + + + + org.jadira.usertype + usertype.extended + 3.2.0.GA + + + + org.springframework.data + spring-data-jpa + + + + org.springframework + spring-aspects + + + org.springframework + spring-context-support + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet + jstl + + + org.springframework + spring-webmvc + + + org.hibernate + hibernate-validator + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + + + javax.el + javax.el-api + test + + + org.glassfish.web + el-impl + 2.2 + test + + + junit + junit + test + + + com.nitorcreations + junit-runners + 1.3 + test + + + org.assertj + assertj-core + 3.1.0 + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + info.solidsoft.mockito + mockito-java8 + 0.3.0 + test + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.security.version} + test + + + com.jayway.jsonpath + json-path + test + + + com.jayway.jsonpath + json-path-assert + 0.9.1 + test + + + com.github.springtestdbunit + spring-test-dbunit + 1.2.1 + test + + + org.dbunit + dbunit + 2.5.1 + test + + + junit + junit + + + + + + ROOT + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-integration-test-sources + generate-test-sources + + add-test-source + + + + src/integration-test/java + + + + + add-integration-test-resources + generate-test-resources + + add-test-resource + + + + + src/integration-test/resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${jdk.version} + ${jdk.version} + ${project.build.sourceEncoding} + + + + + org.bsc.maven + maven-processor-plugin + 2.2.4 + + + process + + process + + generate-sources + + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor + + + + + + + org.hibernate + hibernate-jpamodelgen + 4.3.8.Final + + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + ROOT + false + + + frontend/build + / + false + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + ${skip.unit.tests} + + **/IT*.java + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18 + + + + integration-tests + + integration-test + verify + + + + ${skip.integration.tests} + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.2.10.v20150310 + + 0 + stop + 9999 + + + spring.profiles.active + application + + + + ${project.basedir}/target/ROOT.war + / + + ${project.basedir}/src/main/webapp + ${project.basedir}/frontend/build + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + + run + + + + + + + diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java new file mode 100644 index 0000000..3e294d5 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * This class contains the constants that are used in our integration tests, DbUnit datasets, + * and the localization file. + * + * @author Petri Kainulainen + */ +public final class TodoConstants { + + public static class TodoEntries { + + public static class First { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "description"; + public static final Long ID = 1L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "title"; + } + + public static class Second { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "tiscription"; + public static final Long ID = 2L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "First"; + + } + } + + public static final String SEARCH_TERM_DESCRIPTION_MATCHES = "esC"; + public static final String SEARCH_TERM_NO_MATCH = "NO MATCH"; + public static final String SEARCH_TERM_TITLE_MATCHES = "It"; + + public static final String UPDATED_DESCRIPTION = "updatedDescription"; + public static final String UPDATED_TITLE = "updatedTitle"; + + public static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; + public static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; + public static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; + public static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; + + /** + * Prevents instantiation + */ + private TodoConstants() {} +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java new file mode 100644 index 0000000..77cdb31 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * @author Petri Kainulainen + */ +public enum Users { + + USER("user", "password", "ROLE_USER"); + + private String password; + private String role; + private String username; + + Users(String username, String password, String role) { + this.password = password; + this.role = role; + this.username = username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } + + public String getUsername() { + return username; + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java new file mode 100644 index 0000000..fdb35ca --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java @@ -0,0 +1,93 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; + +import static net.petrikainulainen.springdata.jpa.todo.TodoSpecifications.titleOrDescriptionContainsIgnoreCase; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITTodoRepositoryTest { + + @Autowired + private TodoRepository repository; + + @Test + public void findBySearchTerm_SearchTermIsNull_ShouldReturnTwoTodoEntries() { + List todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(null)); + + assertThat(todoEntries).hasSize(2); + + Todo firstTodoEntry = todoEntries.get(0); + assertThat(firstTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + + Todo secondTodoEntry = todoEntries.get(1); + assertThat(secondTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findBySearchTerm_SearchTermIsEmpty_ShouldReturnTwoTodoEntries() { + List todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase("")); + + assertThat(todoEntries).hasSize(2); + + Todo firstTodoEntry = todoEntries.get(0); + assertThat(firstTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + + Todo secondTodoEntry = todoEntries.get(1); + assertThat(secondTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findBySearchTerm_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES)); + + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnEmptyList() { + List todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_NO_MATCH)); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTerm_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_TITLE_MATCHES)); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java new file mode 100644 index 0000000..af912d1 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java @@ -0,0 +1,27 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.dataset.FlatXmlDataSetLoader; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ReplacementDataSet; +import org.springframework.core.io.Resource; +/** + * This class is a custom DbUnit data set loader that support flat XML data sets. This data set loader + * adds support for the extra features: + *
    + *
  • You can use the column sensing feature of DbUnit.
  • + *
  • You can specify that a column's value is null by using the string [null].
  • + *
+ * @author Petri Kainulainen + */ +public class ColumnSensingReplacementDataSetLoader extends FlatXmlDataSetLoader { + + @Override + protected IDataSet createDataSet(Resource resource) throws Exception { + return createReplacementDataSet(super.createDataSet(resource)); + } + private ReplacementDataSet createReplacementDataSet(IDataSet dataSet) { + ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet); + replacementDataSet.addReplacementObject("[null]", null); + return replacementDataSet; + } +} \ No newline at end of file diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java new file mode 100644 index 0000000..4360756 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Petri Kainulainen + */ +public final class DbTestUtil { + + private DbTestUtil() {} + + public static void resetAutoIncrementColumns(ApplicationContext applicationContext, + String... tableNames) throws SQLException { + DataSource dataSource = applicationContext.getBean(DataSource.class); + String resetSqlTemplate = getResetSqlTemplate(applicationContext); + try (Connection dbConnection = dataSource.getConnection()) { + //Create SQL statements that reset the auto increment columns and invoke + //the created SQL statements. + for (String resetSqlArgument: tableNames) { + try (Statement statement = dbConnection.createStatement()) { + String resetSql = String.format(resetSqlTemplate, resetSqlArgument); + statement.execute(resetSql); + } + } + } + } + + private static String getResetSqlTemplate(ApplicationContext applicationContext) { + //Read the SQL template from the properties file + Environment environment = applicationContext.getBean(Environment.class); + return environment.getRequiredProperty("test.reset.sql.template"); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java new file mode 100644 index 0000000..06b9c1c --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java @@ -0,0 +1,251 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +@DatabaseSetup("no-todo-entries.xml") +public class ITCreateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + DbTestUtil.resetAutoIncrementColumns(webAppContext, "todos"); + + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void create_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isCreated()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.creationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @ExpectedDatabase(value = "create-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java new file mode 100644 index 0000000..ed8aca6 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITDeleteTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void delete_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("delete-todo-entry-expected.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java new file mode 100644 index 0000000..5781ca7 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java @@ -0,0 +1,97 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindAllTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findAll_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findAll_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenTodoEntriesAreNotFound_ShouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenOneTodoEntryIsFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java new file mode 100644 index 0000000..393d146 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java @@ -0,0 +1,107 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindByIdTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void findById_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java new file mode 100644 index 0000000..cbec063 --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java @@ -0,0 +1,318 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("two-todo-entries.xml") +public class ITFindBySearchTermTest { + + private static final int FIRST_PAGE = 0; + private static final String FIRST_PAGE_STRING = "0"; + + private static final int PAGE_SIZE = 1; + private static final String PAGE_SIZE_STRING = "1"; + + private static final String SEARCH_TERM = "tIo"; + private static final int SECOND_PAGE = 1; + private static final String SECOND_PAGE_STRING = "1"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + /* Response status tests */ + @Test + public void findBySearchTerm_AsAnonymous_ShouldReturnHttpResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isOk()); + } + + + /* No results found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnEmptyPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.numberOfElements", is(0))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnPageThatHasZeroTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + /* One todo entry found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + /* Pagination tests */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnFirstPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(FIRST_PAGE))) + .andExpect(jsonPath("$.first", is(true))) + .andExpect(jsonPath("$.last", is(false))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnSecondTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.Second.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.Second.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.Second.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.Second.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.Second.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.Second.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.Second.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnLastPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(SECOND_PAGE))) + .andExpect(jsonPath("$.first", is(false))) + .andExpect(jsonPath("$.last", is(true))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnFirstTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java new file mode 100644 index 0000000..fc0308b --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java @@ -0,0 +1,327 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITUpdateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void update_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.UPDATED_DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.UPDATED_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase(value = "update-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java new file mode 100644 index 0000000..e410ded --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java @@ -0,0 +1,79 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import net.petrikainulainen.springdata.jpa.web.WebTestConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITGetAuthenticatedUserTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void getAuthenticatedUser_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnUserInformationAsJSON() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.username", is("user"))) + .andExpect(jsonPath("$.role", is(UserRole.ROLE_USER.name()))); + } +} diff --git a/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java new file mode 100644 index 0000000..a7d93ff --- /dev/null +++ b/criteria-api/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITLoginTest { + + private static final String INVALID_PASSWORD = "invalidPassword"; + private static final String INVALID_USERNAME = "invalidUsername"; + + private static final String PARAM_NAME_PASSWORD = "password"; + private static final String PARAM_NAME_USERNAME = "username"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void logIn_WhenUsernameIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, INVALID_USERNAME) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenPasswordIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, INVALID_PASSWORD) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldReturnResponseStatusFound() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isFound()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldRedirectClientToControllerMethodThatReturnsAuthenticatedUser() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(redirectedUrl("/api/authenticated-user")); + } +} diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml new file mode 100644 index 0000000..5a18c38 --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml new file mode 100644 index 0000000..12e0c00 --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/todo-entries.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml similarity index 71% rename from query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/todo-entries.xml rename to criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml index a3be6c7..50193f2 100644 --- a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/todo-entries.xml +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml @@ -1,7 +1,9 @@ diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml new file mode 100644 index 0000000..0c1e6bc --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml new file mode 100644 index 0000000..fbb3e27 --- /dev/null +++ b/criteria-api/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/criteria-api/src/main/ant/build.xml b/criteria-api/src/main/ant/build.xml new file mode 100644 index 0000000..90d4c18 --- /dev/null +++ b/criteria-api/src/main/ant/build.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java new file mode 100644 index 0000000..6a9566b --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * This class obtains the current time by using a {@link DateTimeService} + * object. The reason for this is that we can use a different implementation in our integration tests. + * + * In other words: + *
    + *
  • + * Our application always returns the correct time because it uses the + * {@link CurrentTimeDateTimeService} class. + *
  • + *
  • + * When our integration tests are running, we can return a constant time which gives us the possibility + * to assert the creation and modification times saved to the database. + *
  • + *
+ * + * @author Petri Kainulainen + */ +public class AuditingDateTimeProvider implements DateTimeProvider { + + private final DateTimeService dateTimeService; + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Calendar getNow() { + return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java new file mode 100644 index 0000000..424e1d4 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * This class is used in our integration tests and it always returns the + * same time. This gives us the possibility to verify that the correct + * timestamps are saved to the database. + * + * @author Petri Kainulainen + */ +public class ConstantDateTimeService implements DateTimeService { + + public static final String CURRENT_DATE_AND_TIME = getConstantDateAndTime(); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConstantDateTimeService.class); + + private static String getConstantDateAndTime() { + return "2015-07-19T12:52:28" + + getSystemZoneOffset() + + getSystemZoneId(); + } + + private static String getSystemZoneOffset() { + return ZonedDateTime.now().getOffset().toString(); + } + + private static String getSystemZoneId() { + return "[" + ZoneId.systemDefault().toString() + "]"; + } + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime constantDateAndTime = ZonedDateTime.from(FORMATTER.parse(CURRENT_DATE_AND_TIME)); + + LOGGER.info("Returning constant date and time: {}", constantDateAndTime); + + return constantDateAndTime; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java new file mode 100644 index 0000000..2812fb0 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +/** + * This class returns the current time. + * + * @author Petri Kainulainen + */ +public class CurrentTimeDateTimeService implements DateTimeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CurrentTimeDateTimeService.class); + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime currentDateAndTime = ZonedDateTime.now(); + + LOGGER.info("Returning current date and time: {}", currentDateAndTime); + + return currentDateAndTime; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java new file mode 100644 index 0000000..a1e1a11 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java @@ -0,0 +1,18 @@ +package net.petrikainulainen.springdata.jpa.common; + +import java.time.ZonedDateTime; + +/** + * This interface defines the methods used to get the current + * date and time. + * + * @author Petri Kainulainen + */ +public interface DateTimeService { + + /** + * Returns the current date and time. + * @return + */ + ZonedDateTime getCurrentDateAndTime(); +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java new file mode 100644 index 0000000..46f2849 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * This controller is responsible of starting the frontend application. + * @author Petri Kainulainen + */ +@Controller +public class FrontendLoaderController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FrontendLoaderController.class); + + private static final String FRONTEND_APPLICATION_VIEW = "frontend/client"; + + /** + * Starts the AngularJS application. + * @return + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public String startAngularJSApplication() { + LOGGER.debug("Starting frontend single page application."); + return FRONTEND_APPLICATION_VIEW; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java new file mode 100644 index 0000000..d3cf557 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.common; + +/** + * This class provides static utility methods that are used to ensure that a constructor or a method was invoked properly. + * These methods throw an exception if the specified precondition is violated. + * + * This class selects the thrown exception by using the guideline given in Effective Java by Joshua Bloch (Item 60). + * + * @author Petri Kainulainen + */ +public final class PreCondition { + + private PreCondition() {} + + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessageTemplate The template that is used to construct the message of the exception thrown if the + * inspected exception is false. The template must use the syntax that is supported + * by the {@link java.lang.String#format(String, Object...)} method. + * @param errorMessageArguments The arguments that are used when the message of the thrown exception is constructed. + * @throws java.lang.IllegalArgumentException if the inspected exception is false. + */ + public static void isTrue(boolean expression, String errorMessageTemplate, Object... errorMessageArguments) { + isTrue(expression, String.format(errorMessageTemplate, errorMessageArguments)); + } + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessage The error message that is passed forward to the exception that is thrown + * if the expression is false. + * @throws java.lang.IllegalArgumentException if the inspected expression is false. + */ + public static void isTrue(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the string given as a method parameter is not empty. + * @param string The inspected string. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the string is empty. + * @throws java.lang.IllegalArgumentException if the inspected string is empty. + */ + public static void notEmpty(String string, String errorMessage) { + if (string.isEmpty()) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the object given as a method parameter is not null. + * @param reference A reference to the inspected object. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the object given as a method parameter is null. + * @throws java.lang.NullPointerException If the object given as a method parameter is null. + */ + public static void notNull(Object reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java new file mode 100644 index 0000000..ed511d8 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java @@ -0,0 +1,34 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/** + * This component returns the username of the authenticated user. + * + * @author Petri Kainulainen + */ +public class UsernameAuditorAware implements AuditorAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(UsernameAuditorAware.class); + + @Override + public String getCurrentAuditor() { + LOGGER.debug("Getting the username of authenticated user."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + LOGGER.debug("Current user is anonymous. Returning null."); + return null; + } + + String username = ((User) authentication.getPrincipal()).getUsername(); + LOGGER.debug("Returning username: {}", username); + + return username; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java new file mode 100644 index 0000000..0f922d8 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.common.CurrentTimeDateTimeService; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * @author Petri Kainulainen + */ +@Configuration +@ComponentScan("net.petrikainulainen.springdata.jpa") +@Import({WebMvcContext.class, PersistenceContext.class, SecurityContext.class}) +public class ExampleApplicationContext { + + private static final String MESSAGE_SOURCE_BASE_NAME = "i18n/messages"; + + /** + * These static classes are required because it makes it possible to use + * different properties files for every Spring profile. + * + * See: This StackOverflow answer for more details. + */ + @Profile(Profiles.APPLICATION) + @Configuration + @PropertySource("classpath:application.properties") + static class ApplicationProperties {} + + @Profile(Profiles.APPLICATION) + @Bean + DateTimeService currentTimeDateTimeService() { + return new CurrentTimeDateTimeService(); + } + + @Profile(Profiles.INTEGRATION_TEST) + @Configuration + @PropertySource("classpath:integration-test.properties") + static class IntegrationTestProperties {} + + @Profile(Profiles.INTEGRATION_TEST) + @Bean + DateTimeService constantDateTimeService() { + return new ConstantDateTimeService(); + } + + @Bean + MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(MESSAGE_SOURCE_BASE_NAME); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } + + @Bean + PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java new file mode 100644 index 0000000..78a9a36 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java @@ -0,0 +1,136 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.petrikainulainen.springdata.jpa.common.AuditingDateTimeProvider; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import net.petrikainulainen.springdata.jpa.common.UsernameAuditorAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +/** + * This configuration class configures the persistence layer of our example application and + * enables annotation driven transaction management. + * + * This configuration is put to a single class because this way we can write integration + * tests for our persistence layer by using the configuration used by our example + * application. In other words, we can ensure that the persistence layer of our application + * works as expected. + * + * @author Petri Kainulainen + */ +@Configuration +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") +@EnableJpaRepositories(basePackages = { + "net.petrikainulainen.springdata.jpa.todo" +}) +@EnableTransactionManagement +@EnableSpringDataWebSupport +class PersistenceContext { + private static final String[] ENTITY_PACKAGES = { + "net.petrikainulainen.springdata.jpa.todo" + }; + + private static final String PROPERTY_NAME_DB_DRIVER_CLASS = "db.driver"; + private static final String PROPERTY_NAME_DB_PASSWORD = "db.password"; + private static final String PROPERTY_NAME_DB_URL = "db.url"; + private static final String PROPERTY_NAME_DB_USER = "db.username"; + private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; + private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; + private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; + private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; + private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; + + @Bean + AuditorAware auditorProvider() { + return new UsernameAuditorAware(); + } + + @Bean + DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { + return new AuditingDateTimeProvider(dateTimeService); + } + + /** + * Creates and configures the HikariCP datasource bean. + * @param env The runtime environment of our application. + * @return + */ + @Bean(destroyMethod = "close") + DataSource dataSource(Environment env) { + HikariConfig dataSourceConfig = new HikariConfig(); + dataSourceConfig.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DB_DRIVER_CLASS)); + dataSourceConfig.setJdbcUrl(env.getRequiredProperty(PROPERTY_NAME_DB_URL)); + dataSourceConfig.setUsername(env.getRequiredProperty(PROPERTY_NAME_DB_USER)); + dataSourceConfig.setPassword(env.getRequiredProperty(PROPERTY_NAME_DB_PASSWORD)); + + return new HikariDataSource(dataSourceConfig); + } + + /** + * Creates the bean that creates the JPA entity manager factory. + * @param dataSource The datasource that provides the database connections. + * @param env The runtime environment of our application. + * @return + */ + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + entityManagerFactoryBean.setPackagesToScan(ENTITY_PACKAGES); + + Properties jpaProperties = new Properties(); + + //Configures the used database dialect. This allows Hibernate to create SQL + //that is optimized for the used database. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); + + //Specifies the action that is invoked to the database when the Hibernate + //SessionFactory is created or closed. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); + + //Configures the naming strategy that is used when Hibernate creates + //new database objects and schema elements + jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); + + //If the value of this property is true, Hibernate writes all SQL + //statements to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); + + //If the value of this property is true, Hibernate will use prettyprint + //when it writes SQL to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); + + entityManagerFactoryBean.setJpaProperties(jpaProperties); + + return entityManagerFactoryBean; + } + + /** + * Creates the transaction manager bean that integrates the used JPA provider with the + * Spring transaction mechanism. + * @param entityManagerFactory The used JPA entity manager factory. + * @return + */ + @Bean + JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java new file mode 100644 index 0000000..bda9711 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.config; + +/** + * This class defines the Spring profiles used in the project. The idea behind this class + * is that it helps us to avoid typos when we are using these profiles. At the moment + * there are two profiles which are described in the following: + *
    + *
  • The APPLICATION profile is used when we run our example application.
  • + *
  • The INTEGRATION_TEST profile is used when we run the integration tests of our example application.
  • + *
+ * + * @author Petri Kainulainen + */ +public class Profiles { + public static final String APPLICATION = "application"; + public static final String INTEGRATION_TEST = "integrationtest"; + /** + * Prevent instantiation. + */ + private Profiles() {} +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java new file mode 100644 index 0000000..8aa95e4 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.web.security.CsrfHeaderFilter; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationEntryPoint; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationFailureHandler; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationSuccessHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CsrfFilter; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebSecurity +class SecurityContext extends WebSecurityConfigurerAdapter { + + @Bean + AuthenticationEntryPoint authenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + AuthenticationFailureHandler authenticationFailureHandler() { + return new RestAuthenticationFailureHandler(); + } + + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler() { + return new RestAuthenticationSuccessHandler(); + } + + @Bean + protected UserDetailsService userDetailsService() { + return super.userDetailsService(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + //Use the custom authentication entry point. + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint()) + .and() + //Configure form login. + .formLogin() + .loginProcessingUrl("/api/login") + .failureHandler(authenticationFailureHandler()) + .successHandler(authenticationSuccessHandler()) + .permitAll() + .and() + //Configure logout function. + .logout() + .deleteCookies("JSESSIONID") + .logoutUrl("/api/logout") + .logoutSuccessUrl("/") + .and() + //Configure url based authorization + .authorizeRequests() + .antMatchers( + "/", + "/api/csrf" + ).permitAll() + .anyRequest().hasRole("USER") + .and() + .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + //Spring Security ignores request to static resources such as CSS or JS files. + .ignoring() + .antMatchers( + "/favicon.ico", + "/css/**", + "/i18n/**", + "/js/**" + ); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java new file mode 100644 index 0000000..f1861a6 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import java.util.EnumSet; + +/** + * @author Petri Kainulainen + */ +public class WebAppConfig implements WebApplicationInitializer { + private static final String CHARACTER_ENCODING_FILTER_ENCODING = "UTF-8"; + private static final String CHARACTER_ENCODING_FILTER_NAME = "characterEncoding"; + private static final String CHARACTER_ENCODING_FILTER_URL_PATTERN = "/*"; + + private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; + private static final String DISPATCHER_SERVLET_MAPPING = "/"; + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(ExampleApplicationContext.class); + + //XmlWebApplicationContext rootContext = new XmlWebApplicationContext(); + //rootContext.setConfigLocation("classpath:applicationContext.xml"); + + configureDispatcherServlet(servletContext, rootContext); + EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + + configureCharacterEncodingFilter(servletContext, dispatcherTypes); + configureSpringSecurityFilter(servletContext, dispatcherTypes); + servletContext.addListener(new ContextLoaderListener(rootContext)); + } + + private void configureDispatcherServlet(ServletContext servletContext, WebApplicationContext rootContext) { + ServletRegistration.Dynamic dispatcher = servletContext.addServlet( + DISPATCHER_SERVLET_NAME, + new DispatcherServlet(rootContext) + ); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); + } + + private void configureCharacterEncodingFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + characterEncodingFilter.setEncoding(CHARACTER_ENCODING_FILTER_ENCODING); + characterEncodingFilter.setForceEncoding(true); + FilterRegistration.Dynamic characterEncoding = servletContext.addFilter(CHARACTER_ENCODING_FILTER_NAME, characterEncodingFilter); + characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, CHARACTER_ENCODING_FILTER_URL_PATTERN); + } + + private void configureSpringSecurityFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); + security.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java new file mode 100644 index 0000000..c016860 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebMvc +class WebMvcContext extends WebMvcConfigurerAdapter { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + + @Override + public void configureMessageConverters(List> converters) { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + + converters.add(converter); + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp("/WEB-INF/jsp/", ".jsp"); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java new file mode 100644 index 0000000..bb18e3c --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java @@ -0,0 +1,42 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static net.petrikainulainen.springdata.jpa.todo.TodoSpecifications.titleOrDescriptionContainsIgnoreCase; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoSearchService implements TodoSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoSearchService.class); + + private final TodoRepository repository; + + @Autowired + public RepositoryTodoSearchService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + @Override + public Page findBySearchTerm(String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = repository.findAll(titleOrDescriptionContainsIgnoreCase(searchTerm), pageRequest); + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return TodoMapper.mapEntityPageIntoDTOPage(pageRequest, searchResultPage); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java new file mode 100644 index 0000000..f59be0d --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoService implements TodoCrudService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoService.class); + + private final TodoRepository repository; + + @Autowired + RepositoryTodoService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional + @Override + public TodoDTO create(TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + Todo created = Todo.getBuilder() + .description(newTodoEntry.getDescription()) + .title(newTodoEntry.getTitle()) + .build(); + + created = repository.save(created); + LOGGER.info("Created a new todo entry: {}", created); + + return TodoMapper.mapEntityIntoDTO(created); + } + + @Transactional + @Override + public TodoDTO delete(Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + Todo deleted = findTodoEntryById(id); + LOGGER.debug("Found todo entry: {}", deleted); + + repository.delete(deleted); + LOGGER.info("Deleted todo entry: {}", deleted); + + return TodoMapper.mapEntityIntoDTO(deleted); + } + + @Transactional(readOnly = true) + @Override + public List findAll() { + LOGGER.info("Finding all todo entries."); + + List todoEntries = repository.findAll(); + + LOGGER.info("Found {} todo entries", todoEntries.size()); + + return TodoMapper.mapEntitiesIntoDTOs(todoEntries); + } + + @Transactional(readOnly = true) + @Override + public TodoDTO findById(Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + Todo todoEntry = findTodoEntryById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return TodoMapper.mapEntityIntoDTO(todoEntry); + } + + @Transactional + @Override + public TodoDTO update(TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + Todo updated = findTodoEntryById(updatedTodoEntry.getId()); + updated.update(updatedTodoEntry.getTitle(), updatedTodoEntry.getDescription()); + + //We need to flush the changes or otherwise the returned object + //doesn't contain the updated audit information. + repository.flush(); + + LOGGER.info("Updated the information of the todo entry: {}", updated); + + return TodoMapper.mapEntityIntoDTO(updated); + } + + private Todo findTodoEntryById(Long id) { + Optional todoResult = repository.findOne(id); + return todoResult.orElseThrow(() -> new TodoNotFoundException(id)); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java new file mode 100644 index 0000000..4eccac6 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java @@ -0,0 +1,181 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.annotations.Type; +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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Version; +import java.time.ZonedDateTime; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.isTrue; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This entity class contains the information of a single todo entry + * and the methods that are used to create new todo entries and to modify + * the information of an existing todo entry. + * + * @author Petri Kainulainen + */ +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "todos") +final class Todo { + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(name = "created_by_user", nullable = false) + @CreatedBy + private String createdByUser; + + @Column(name = "creation_time", nullable = false) + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @CreatedDate + private ZonedDateTime creationTime; + + @Column(name = "description", length = MAX_LENGTH_DESCRIPTION) + private String description; + + @Column(name = "modified_by_user", nullable = false) + @LastModifiedBy + private String modifiedByUser; + + @Column(name = "modification_time") + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @LastModifiedDate + private ZonedDateTime modificationTime; + + @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE) + private String title; + + @Version + private long version; + + /** + * Required by Hibernate. + */ + private Todo() {} + + private Todo(Builder builder) { + this.title = builder.title; + this.description = builder.description; + } + + static Builder getBuilder() { + return new Builder(); + } + + Long getId() { + return id; + } + + String getCreatedByUser() { + return createdByUser; + } + + ZonedDateTime getCreationTime() { + return creationTime; + } + + String getDescription() { + return description; + } + + String getModifiedByUser() { + return modifiedByUser; + } + + ZonedDateTime getModificationTime() { + return modificationTime; + } + + String getTitle() { + return title; + } + + long getVersion() { + return version; + } + + void update(String newTitle, String newDescription) { + requireValidTitleAndDescription(newTitle, newDescription); + + this.title = newTitle; + this.description = newDescription; + } + + private void requireValidTitleAndDescription(String title, String description) { + notNull(title, "Title cannot be null."); + notEmpty(title, "Title cannot be empty."); + isTrue(title.length() <= MAX_LENGTH_TITLE, + "The maximum length of the title is <%d> characters.", + MAX_LENGTH_TITLE + ); + + isTrue((description == null) || (description.length() <= MAX_LENGTH_DESCRIPTION), + "The maximum length of the description is <%d> characters.", + MAX_LENGTH_DESCRIPTION + ); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .append("version", this.version) + .toString(); + } + + /** + * This entity is so simple that you don't really need to use the builder pattern + * (use a constructor instead). I use the builder pattern here because it makes + * the code a bit more easier to read. + */ + static class Builder { + private String description; + private String title; + + private Builder() {} + + Builder description(String description) { + this.description = description; + return this; + } + + Builder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = new Todo(this); + + build.requireValidTitleAndDescription(build.getTitle(), build.getDescription()); + + return build; + } + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java new file mode 100644 index 0000000..9e6fc09 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java @@ -0,0 +1,49 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +public interface TodoCrudService { + + /** + * Creates a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + TodoDTO create(TodoDTO newTodoEntry); + + /** + * Deletes a todo entry from the database. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + TodoDTO delete(Long id); + + /** + * Finds all todo entries that are saved to the database. + * @return + */ + List findAll(); + + /** + * Finds a todo entry by using the id given as a method parameter. + * @param id The id of the wanted todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO findById(Long id); + + /** + * Updates the information of an existing information. + * @param updatedTodoEntry The new information of an existing todo entry. + * @return The information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO update(TodoDTO updatedTodoEntry); +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java new file mode 100644 index 0000000..7eea8d2 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Size; +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTO { + + private String createdByUser; + + private ZonedDateTime creationTime; + + @Size(max = Todo.MAX_LENGTH_DESCRIPTION) + private String description; + + private Long id; + + private String modifiedByUser; + + private ZonedDateTime modificationTime; + + @NotEmpty + @Size(max = Todo.MAX_LENGTH_TITLE) + private String title; + + public TodoDTO() {} + + public String getCreatedByUser() { + return createdByUser; + } + + public ZonedDateTime getCreationTime() { + return creationTime; + } + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + public String getModifiedByUser() { + return modifiedByUser; + } + + public ZonedDateTime getModificationTime() { + return modificationTime; + } + + public String getTitle() { + return title; + } + + public void setCreatedByUser(String createdByUser) { + this.createdByUser = createdByUser; + } + + public void setCreationTime(ZonedDateTime creationTime) { + this.creationTime = creationTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setId(Long id) { + this.id = id; + } + + public void setModifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + + public void setModificationTime(ZonedDateTime modificationTime) { + this.modificationTime = modificationTime; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .toString(); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java new file mode 100644 index 0000000..006c80a --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java @@ -0,0 +1,40 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Petri Kainulainen + */ +final class TodoMapper { + + static List mapEntitiesIntoDTOs(List entities) { + return entities.stream() + .map(TodoMapper::mapEntityIntoDTO) + .collect(toList()); + } + + static TodoDTO mapEntityIntoDTO(Todo entity) { + TodoDTO dto = new TodoDTO(); + + dto.setCreatedByUser(entity.getCreatedByUser()); + dto.setCreationTime(entity.getCreationTime()); + dto.setDescription(entity.getDescription()); + dto.setId(entity.getId()); + dto.setModifiedByUser(entity.getModifiedByUser()); + dto.setModificationTime(entity.getModificationTime()); + dto.setTitle(entity.getTitle()); + + return dto; + } + + static Page mapEntityPageIntoDTOPage(Pageable page, Page source) { + List dtos = mapEntitiesIntoDTOs(source.getContent()); + return new PageImpl<>(dtos, page, source.getTotalElements()); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java new file mode 100644 index 0000000..63f6948 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * This exception is thrown when a todo entry is not found by + * using the given id. + * + * @author Petri Kainulainen + */ +public class TodoNotFoundException extends RuntimeException { + + private final Long id; + + public TodoNotFoundException(Long id) { + super(); + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java new file mode 100644 index 0000000..88d51c5 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * This repository provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +interface TodoRepository extends Repository, JpaSpecificationExecutor { + + void delete(Todo deleted); + + List findAll(); + + Optional findOne(Long id); + + void flush(); + + Todo save(Todo persisted); +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java new file mode 100644 index 0000000..41212eb --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * This service provides finder methods for {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +public interface TodoSearchService { + + /** + * Finds todo entries whose title or description contains the given search term. + * This search is case insensitive. + * @param searchTerm The search term. + * @param pageRequest The information of the requested page. + * @return + */ + Page findBySearchTerm(String searchTerm, Pageable pageRequest); +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSpecifications.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSpecifications.java new file mode 100644 index 0000000..46a0388 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSpecifications.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.jpa.domain.Specification; + +/** + * This specification builder class provides static methods that are used + * to create Specification<Todo> objects which specify + * the search criteria of dynamic database queries. + * + * @author Petri Kainulainen + */ +final class TodoSpecifications { + + /** + * Prevent instantiation. + */ + private TodoSpecifications() {} + + /** + * Creates the search criteria that returns all todo entries whose title or description + * contains the given search term. The search is case insensitive. + * + * If the search term is null or empty, the created search criteria will return + * all todo entries. + * + * @param searchTerm The used search term. + * @return + */ + static Specification titleOrDescriptionContainsIgnoreCase(String searchTerm) { + return (root, query, cb) -> { + String containsLikePattern = getContainsLikePattern(searchTerm); + return cb.or( + cb.like(cb.lower(root.get(Todo_.title)), containsLikePattern), + cb.like(cb.lower(root.get(Todo_.description)), containsLikePattern) + ); + }; + } + + private static String getContainsLikePattern(String searchTerm) { + if (searchTerm == null || searchTerm.isEmpty()) { + return "%"; + } + else { + return "%" + searchTerm.toLowerCase() + "%"; + } + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java new file mode 100644 index 0000000..5205657 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java @@ -0,0 +1,116 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * This controller provides the public API that is used to perform + * CRUD operations for todo entries. + * + * @author Petri Kainulainen + */ +@RestController +@RequestMapping("/api/todo") +final class TodoController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoController.class); + + private final TodoCrudService crudService; + + @Autowired + TodoController(TodoCrudService crudService) { + this.crudService = crudService; + } + + /** + * Create a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + TodoDTO create(@RequestBody @Valid TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + TodoDTO created = crudService.create(newTodoEntry); + LOGGER.info("Created a new todo entry: {}", created); + + return created; + } + + /** + * Deletes a todo entry. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public TodoDTO delete(@PathVariable("id") Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + TodoDTO deleted = crudService.delete(id); + LOGGER.info("Deleted the todo entry: {}", deleted); + + return deleted; + } + + /** + * Finds all todo entries. + * + * @return The information of all todo entries. + */ + @RequestMapping(method = RequestMethod.GET) + List findAll() { + LOGGER.info("Finding all todo entries"); + + List todoEntries = crudService.findAll(); + LOGGER.info("Found {} todo entries.", todoEntries.size()); + + return todoEntries; + } + + /** + * Finds a single todo entry. + * @param id The id of the requested todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.GET) + TodoDTO findById(@PathVariable("id") Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + TodoDTO todoEntry = crudService.findById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return todoEntry; + } + + /** + * Updates the information of an existing todo entry. + * @param updatedTodoEntry The new information of the updated todo entry. + * @return The updated information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.PUT) + TodoDTO update(@RequestBody @Valid TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + TodoDTO updated = crudService.update(updatedTodoEntry); + LOGGER.info("Updated the information of the todo entrY: {}", updated); + + return updated; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java new file mode 100644 index 0000000..6ea5cd7 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to find todo entries by using + * different search criteria. + * + * @author Petri Kainulainen + */ +@RestController +final class TodoSearchController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoSearchController.class); + + private final TodoSearchService searchService; + + @Autowired + public TodoSearchController(TodoSearchService searchService) { + this.searchService = searchService; + } + + /** + * Finds todo entries whose title or description contains the given search term. This + * search is case insensitive. + * @param searchTerm The used search term. + * @param pageRequest The information of the requested page + * @return + */ + @RequestMapping(value = "/api/todo/search", method = RequestMethod.GET) + public Page findBySearchTerm(@RequestParam("searchTerm") String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = searchService.findBySearchTerm(searchTerm, pageRequest); + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return searchResultPage; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ErrorDTO.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java similarity index 94% rename from query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ErrorDTO.java rename to criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java index 834b298..b02059e 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ErrorDTO.java +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java @@ -1,4 +1,4 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTO.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java similarity index 93% rename from query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTO.java rename to criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java index b567f86..44234a5 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTO.java +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java @@ -1,4 +1,4 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandler.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java similarity index 96% rename from query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandler.java rename to criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java index 5303b61..5ad9e9b 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandler.java +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java @@ -1,4 +1,4 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; import org.slf4j.Logger; @@ -25,7 +25,7 @@ * @author Petri Kainulainen */ @ControllerAdvice -final class RestErrorHandler { +public final class RestErrorHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class); @@ -34,7 +34,7 @@ final class RestErrorHandler { private final MessageSource messageSource; @Autowired - RestErrorHandler(MessageSource messageSource) { + public RestErrorHandler(MessageSource messageSource) { this.messageSource = messageSource; } diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTO.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java similarity index 93% rename from query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTO.java rename to criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java index 6d4ad22..8355c7b 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTO.java +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java @@ -1,4 +1,4 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import org.springframework.http.HttpStatus; diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java new file mode 100644 index 0000000..141a948 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java @@ -0,0 +1,46 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This filter reads the {@link org.springframework.security.web.csrf.CsrfToken} from the {@link HttpServletRequest} and + * sets its content to the {@link HttpServletResponse} headers. + * + * I borrowed this idea from this StackOverflow question. + * + * @author Petri Kainulainen + */ +public class CsrfHeaderFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(CsrfHeaderFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + LOGGER.trace("Reading CSRF token from the request."); + + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (token != null) { + LOGGER.trace("CSRF token was found. Creating HTTP response headers."); + response.setHeader("X-CSRF-HEADER", token.getHeaderName()); + response.setHeader("X-CSRF-PARAM", token.getParameterName()); + response.setHeader("X-CSRF-TOKEN", token.getToken()); + } + else { + LOGGER.trace("CSRF Token was not found. Doing nothing."); + } + + filterChain.doFilter(request, response); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java new file mode 100644 index 0000000..f6e70cb --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Petri Kainulainen + */ +@RestController +public class CsrfTokenController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsrfTokenController.class); + + @RequestMapping(value = "/api/csrf", method = RequestMethod.HEAD) + public void getCsrfToken() { + LOGGER.info("Getting CSRF token."); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..887e25b --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication entry point returns the HTTP status code 401. + * @author Petri Kainulainen + */ +public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + LOGGER.info("Authentication required. Returning HTTP status code 401."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..daf635b --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication failure handler returns the HTTP status code 403. + * @author Petri Kainulainen + */ +public final class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationFailureHandler.class); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + LOGGER.info("Authentication failed with message: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failed."); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ff84785 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication success handler returns the information of the authenticated + * user as JSON. + * + * @author Petri Kainulainen + */ +public final class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + LOGGER.info("Authentication was successful"); + response.sendRedirect(response.encodeRedirectURL("/api/authenticated-user")); + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java new file mode 100644 index 0000000..ef7959d --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java @@ -0,0 +1,45 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to return the information + * of the authenticated user. + * + * @author Petri Kainulainen + */ +@RestController +final class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + /** + * Returns the information of the authenticated user as JSON. The returned information + * contains the username and the user role of the authenticated user. + * + * @param authenticatedUser The information of the authenticated user. + * @return + */ + @RequestMapping(value = "/api/authenticated-user", method = RequestMethod.GET) + public UserDTO getAuthenticatedUser(@AuthenticationPrincipal User authenticatedUser) { + LOGGER.info("Getting authenticated user."); + + if (authenticatedUser == null) { + //If anonymous users can access this controller method, someone has changed + //the security configuration and it must be fixed. + LOGGER.error("Authenticated user is not found."); + throw new AccessDeniedException("Anonymous users cannot request the information of the authenticated user."); + } + else { + LOGGER.info("User with username: {} is authenticated", authenticatedUser.getUsername()); + return new UserDTO(authenticatedUser.getUsername(), authenticatedUser.getAuthorities()); + } + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java new file mode 100644 index 0000000..92b99ed --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import net.petrikainulainen.springdata.jpa.common.PreCondition; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * This class contains the information of the authenticated user. + * + * @author Petri Kainulainen + */ +public final class UserDTO { + + private final String username; + + private final UserRole role; + + UserDTO(String username, Collection authorities) { + PreCondition.isTrue(!username.isEmpty(), "Username cannot be empty."); + PreCondition.isTrue(authorities.size() == 1, "User must have only one granted authority."); + this.username = username; + + GrantedAuthority authority = authorities.iterator().next(); + this.role = UserRole.valueOf(authority.getAuthority()); + } + + public String getUsername() { + return username; + } + + public UserRole getRole() { + return role; + } +} diff --git a/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java new file mode 100644 index 0000000..8b3e6a6 --- /dev/null +++ b/criteria-api/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java @@ -0,0 +1,8 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +/** + * @author Petri Kainulainen + */ +enum UserRole { + ROLE_USER +} diff --git a/criteria-api/src/main/resources/application.properties b/criteria-api/src/main/resources/application.properties new file mode 100644 index 0000000..7ac8298 --- /dev/null +++ b/criteria-api/src/main/resources/application.properties @@ -0,0 +1,12 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false \ No newline at end of file diff --git a/criteria-api/src/main/resources/applicationContext-persistence.xml b/criteria-api/src/main/resources/applicationContext-persistence.xml new file mode 100644 index 0000000..e9149e9 --- /dev/null +++ b/criteria-api/src/main/resources/applicationContext-persistence.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${hibernate.dialect} + + + ${hibernate.hbm2ddl.auto} + + + ${hibernate.ejb.naming_strategy} + + + ${hibernate.show_sql} + + + ${hibernate.format_sql} + + + + + + + + + + + + + \ No newline at end of file diff --git a/criteria-api/src/main/resources/applicationContext-web.xml b/criteria-api/src/main/resources/applicationContext-web.xml new file mode 100644 index 0000000..db48af6 --- /dev/null +++ b/criteria-api/src/main/resources/applicationContext-web.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + WRITE_DATES_AS_TIMESTAMPS + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/criteria-api/src/main/resources/applicationContext.xml b/criteria-api/src/main/resources/applicationContext.xml new file mode 100644 index 0000000..b9ee424 --- /dev/null +++ b/criteria-api/src/main/resources/applicationContext.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/criteria-api/src/main/resources/i18n/messages.properties b/criteria-api/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..0e376f5 --- /dev/null +++ b/criteria-api/src/main/resources/i18n/messages.properties @@ -0,0 +1,5 @@ +error.todo.entry.not.found=No todo entry was found by using id: {0} + +NotEmpty.todoDTO.title=The title cannot be empty +Size.todoDTO.description=The maximum length of description is 500 characters +Size.todoDTO.title=The maximum length of title is 100 characters \ No newline at end of file diff --git a/criteria-api/src/main/resources/integration-test.properties b/criteria-api/src/main/resources/integration-test.properties new file mode 100644 index 0000000..3605c55 --- /dev/null +++ b/criteria-api/src/main/resources/integration-test.properties @@ -0,0 +1,14 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa;DB_CLOSE_ON_EXIT=FALSE +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false + +test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1 \ No newline at end of file diff --git a/tutorial-part-four/src/main/resources/log4j.properties b/criteria-api/src/main/resources/log4j.properties similarity index 75% rename from tutorial-part-four/src/main/resources/log4j.properties rename to criteria-api/src/main/resources/log4j.properties index 5ad34eb..668d97a 100644 --- a/tutorial-part-four/src/main/resources/log4j.properties +++ b/criteria-api/src/main/resources/log4j.properties @@ -3,4 +3,6 @@ log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG + +log4j.logger.org.hibernate=INFO +log4j.logger.org.springframework=INFO \ No newline at end of file diff --git a/criteria-api/src/main/webapp/WEB-INF/jsp/frontend/client.jsp b/criteria-api/src/main/webapp/WEB-INF/jsp/frontend/client.jsp new file mode 100644 index 0000000..84158d0 --- /dev/null +++ b/criteria-api/src/main/webapp/WEB-INF/jsp/frontend/client.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ +
+
+

+

+
+
+ + + diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java new file mode 100644 index 0000000..c51749a --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Petri Kainulainen + */ +public class PageBuilder { + + private List elements = new ArrayList<>(); + private Pageable pageRequest; + private int totalElements; + + public PageBuilder() {} + + public PageBuilder elements(List elements) { + this.elements = elements; + return this; + } + + public PageBuilder pageRequest(Pageable pageRequest) { + this.pageRequest = pageRequest; + return this; + } + + public PageBuilder totalElements(int totalElements) { + this.totalElements = totalElements; + return this; + } + + public Page build() { + return new PageImpl(elements, pageRequest, totalElements); + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java new file mode 100644 index 0000000..7e90183 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Petri Kainulainen + */ +public class PreConditionTest { + + private static final String STATIC_ERROR_MESSAGE = "static error message"; + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, "Dynamic error message with parameter: %d", 1L); + } + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic error message with parameter: 1"); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, STATIC_ERROR_MESSAGE); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsNotEmpty_ShouldNotThrowException() { + PreCondition.notEmpty(" ", STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsEmpty_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNotNull_ShouldNotThrowException() { + PreCondition.notNull(new Object(), STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNull_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java new file mode 100644 index 0000000..f1fe9a2 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java @@ -0,0 +1,164 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.Arrays; + +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoSearchServiceTest { + + private static final String SEARCH_TERM = "itl"; + + private TodoRepository repository; + private RepositoryTodoSearchService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoSearchService(repository); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final int PAGE_SIZE = 5; + private final String SORT_PROPERTY = "title"; + + private Pageable pageRequest; + + @Before + public void createPageRequest() { + Sort sort = new Sort(Sort.Direction.ASC, SORT_PROPERTY); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findAll(isA(Specification.class), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageWithRequestedPageNumber() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumber()).isEqualTo(PAGE_NUMBER); + } + + @Test + public void shouldReturnPageWithRequestedPageSize() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSize()).isEqualTo(PAGE_SIZE); + } + + @Test + public void shouldReturnPageThatIsSortedInAscendingOrderByUsingSortProperty() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSort().getOrderFor(SORT_PROPERTY).getDirection()) + .isEqualTo(Sort.Direction.ASC); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findAll(isA(Specification.class), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnEmptyPage() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void shouldReturnPageWithTotalElementCountZero() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + } + + public class WhenOneTodoEntryIsFound { + + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final Long ID = 20L; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Page resultPage = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + + given(repository.findAll(isA(Specification.class), eq(pageRequest))).willReturn(resultPage); + } + + @Test + public void shouldReturnPageThatHasOneTodoEntry() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + } + + @Test + public void shouldReturnPageThatHasCorrectInformation() { + TodoDTO found = service.findBySearchTerm(SEARCH_TERM, pageRequest).getContent().get(0); + + assertThatTodoDTO(found) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnPageWithTotalElementCountOne() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + } + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java new file mode 100644 index 0000000..81bbdb5 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java @@ -0,0 +1,387 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoServiceTest { + + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + private static final Long ID = 20L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + private TodoRepository repository; + + private RepositoryTodoService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoService(repository); + } + + public class Create { + + @Before + public void returnNewTodoEntry() { + given(repository.save(isA(Todo.class))).willAnswer( + invocationOnMock -> new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build() + ); + } + + @Test + public void shouldPersistNewTodoEntryWithCorrectInformation() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + service.create(newTodoEntry); + + verify(repository, times(1)).save( + assertArg(persisted -> assertThatTodoEntry(persisted) + .hasNoCreationAuditFieldValues() + .hasDescription(DESCRIPTION) + .hasNoId() + .hasNoModificationAuditFieldValues() + .hasTitle(TITLE) + ) + ); + verifyNoMoreInteractions(repository); + } + + @Test + public void shouldReturnTheInformationOfPersistedTodoEntry() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + TodoDTO created = service.create(newTodoEntry); + assertThatTodoDTO(created) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.delete(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException ex = (TodoNotFoundException) thrown; + assertThat(ex.getId()).isEqualTo(ID); + } + + @Test + public void shouldNotDeleteTodoEntry() { + catchThrowable(() -> service.delete(ID)); + + verify(repository, never()).delete(isA(Todo.class)); + } + } + + public class WhenTodoEntryIsFound { + + private Todo deleted; + + @Before + public void returnDeletedTodoEntry() { + deleted = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(deleted)); + } + + @Test + public void shouldDeleteFoundTodoEntry() { + service.delete(ID); + + verify(repository, times(1)).delete(deleted); + } + + @Test + public void shouldReturnTheInformationOfDeletedTodoEntry() { + TodoDTO deleted = service.delete(ID); + + assertThatTodoDTO(deleted) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindAll { + + public class WhenNoTodoEntryAreFound { + + @Before + public void returnNoTodoEntries() { + given(repository.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).hasSize(1); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO todoEntry = service.findAll().get(0); + + assertThatTodoDTO(todoEntry) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindOne { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.findById(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(found)); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO returned = service.findById(ID); + + assertThatTodoDTO(returned) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + Throwable thrown = catchThrowable(() -> service.update(updatedTodoEntry)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + private Todo updated; + + @Before + public void returnUpdatedTodoEntry() { + updated = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(updated)); + } + + @Test + public void shouldUpdateTitleAndDescription() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldNotUpdateIdOrAuditInformation() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasId(ID) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntry() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + TodoDTO returnedTodoEntry = service.update(updatedTodoEntry); + + assertThatTodoDTO(returnedTodoEntry) + .hasDescription(UPDATED_DESCRIPTION) + .hasId(ID) + .hasTitle(UPDATED_TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java new file mode 100644 index 0000000..ed98667 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author Petri Kainulainen + */ +public final class TestUtil { + + private TestUtil() {} + + public static String createStringWithLength(int length) { + StringBuilder string = new StringBuilder(); + + for (int index = 0; index < length; index++) { + string.append("a"); + } + + return string.toString(); + } + + public static ZonedDateTime parseDateTime(String dateAndTime) { + return ZonedDateTime.parse(dateAndTime, DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java new file mode 100644 index 0000000..e88f27c --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java @@ -0,0 +1,198 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This class provides a fluent API that can be used for writing assertions + * to {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +final class TodoAssert extends AbstractAssert { + + private TodoAssert(Todo actual) { + super(actual, TodoAssert.class); + } + + static TodoAssert assertThatTodoEntry(Todo actual) { + return new TodoAssert(actual); + } + + TodoAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage(String.format( + "Expected description to be <%s> but was <%s>.", + expectedDescription, + actualDescription + )) + .isEqualTo(expectedDescription); + + return this; + } + + TodoAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creationTime to be but was <%s>", + actualCreationTime + ) + .isNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + return this; + } + + TodoAssert hasNoDescription() { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage("Expected description to be but was <%s>", actualDescription) + .isNull(); + + return this; + } + + TodoAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be <%d> but was <%d>", + expectedId, + actualId + ) + .isEqualTo(expectedId); + + return this; + } + + TodoAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>.", actualId) + .isNull(); + + return this; + } + + TodoAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>.", + actualModificationTime + ) + .isNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>", + actualModificationTime + ) + .isNull(); + + return this; + } + + TodoAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>.", + expectedTitle, + actualTitle + ) + .isEqualTo(actualTitle); + + return this; + } + + public TodoAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java new file mode 100644 index 0000000..90ee955 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java @@ -0,0 +1,71 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +class TodoBuilder { + + private Long id; + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + TodoBuilder() {} + + TodoBuilder id(Long id) { + this.id = id; + return this; + } + + TodoBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + TodoBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + TodoBuilder description(String description) { + this.description = description; + return this; + } + + TodoBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + TodoBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + TodoBuilder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = Todo.getBuilder() + .title(title) + .description(description) + .build(); + + ReflectionTestUtils.setField(build, "createdByUser", createdByUser); + ReflectionTestUtils.setField(build, "creationTime", creationTime); + ReflectionTestUtils.setField(build, "id", id); + ReflectionTestUtils.setField(build, "modifiedByUser", modifiedByUser); + ReflectionTestUtils.setField(build, "modificationTime", modificationTime); + + return build; + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java new file mode 100644 index 0000000..462d90d --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java @@ -0,0 +1,179 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTOAssert extends AbstractAssert { + + private TodoDTOAssert(TodoDTO actual) { + super(actual, TodoDTOAssert.class); + } + + public static TodoDTOAssert assertThatTodoDTO(TodoDTO actual) { + return new TodoDTOAssert(actual); + } + + public TodoDTOAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage( + "Expected description to be <%s> but was <%s>", + expectedDescription, + actualDescription + ) + .isEqualTo(expectedDescription); + + return this; + } + + public TodoDTOAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage( + "Expected id to be <%d> but was <%d>", + actualId, + expectedId + ) + .isEqualTo(expectedId); + + return this; + } + + public TodoDTOAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage("Expected creationTime to be but was <%s>", actualCreationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>", actualId) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be but was <%s>", + actualModifiedByUser + ) + .isNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage("Expected modification time to be but was <%d>", actualModificationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>", + expectedTitle, + actualTitle + ) + .isEqualTo(expectedTitle); + + return this; + } + + public TodoDTOAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoDTOAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoDTOAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoDTOAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java new file mode 100644 index 0000000..e0b5505 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java @@ -0,0 +1,68 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public class TodoDTOBuilder { + + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private Long id; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + public TodoDTOBuilder() {} + + public TodoDTOBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + public TodoDTOBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + public TodoDTOBuilder description(String description) { + this.description = description; + return this; + } + + public TodoDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public TodoDTOBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + public TodoDTOBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + public TodoDTOBuilder title(String title) { + this.title = title; + return this; + } + + public TodoDTO build() { + TodoDTO build = new TodoDTO(); + + build.setCreatedByUser(createdByUser); + build.setCreationTime(creationTime); + build.setDescription(description); + build.setId(id); + build.setModifiedByUser(modifiedByUser); + build.setModificationTime(modificationTime); + build.setTitle(title); + + return build; + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java new file mode 100644 index 0000000..c5ff69d --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java @@ -0,0 +1,340 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoTest { + + private static final int MAX_LENGTH_DESCRIPTION = 500; + private static final int MAX_LENGTH_TITLE = 100; + + private static final String DESCRIPTION = "description"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + public class Build { + + public class WhenTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(null) + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title("") + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(tooLongTitle) + .description(DESCRIPTION) + .build(); + } + } + } + + public class WhenDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(TITLE) + .description(tooLongDescription) + .build(); + } + } + + public class WhenTitleAndDescriptionAreValid { + + @Test + public void shouldNotSetId() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoId(); + } + + @Test + public void shouldNotSetCreationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoCreationAuditFieldValues(); + } + + @Test + public void shouldNotSetModificationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoModificationAuditFieldValues(); + } + + @Test + public void shouldSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasDescription(DESCRIPTION); + } + + @Test + public void shouldSetTitle() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(TITLE); + } + + public class WhenMaxLengthTitleIsGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldCreateNewObjectAndSetTitle() { + Todo build = Todo.getBuilder() + .title(maxLengthTitle) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(maxLengthTitle); + } + } + + public class WhenMaxLengthDescriptionIsGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + + } + + @Test + public void shouldCreateNewObjectAndSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(maxLengthDescription) + .build(); + + assertThatTodoEntry(build) + .hasDescription(maxLengthDescription); + } + } + + public class WhenNoDescriptionIsGiven { + + @Test + public void shouldCreateNewObjectWithoutDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .build(); + + assertThatTodoEntry(build) + .hasNoDescription(); + } + } + } + } + + public class Update { + + private Todo updated; + + @Before + public void createUpdatedTodoEntry() { + updated = Todo.getBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + } + + public class WhenNewTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + updated.update(null, UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update("", UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(tooLongTitle, UPDATED_DESCRIPTION); + } + } + } + + public class WhenNewDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(UPDATED_TITLE, tooLongDescription); + } + } + + public class WhenNewTitleAndNewDescriptionAreValid { + + public class WhenMaxLengthTitleAndNewDescriptionAreGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldUpdateTitle() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasTitle(maxLengthTitle); + } + + @Test + public void shouldUpdateDescription() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION); + } + } + + public class WhenNewTitleIsGivenAndNewDescriptionIsNull { + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldRemoveDescription() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasNoDescription(); + } + } + + public class WhenNewTitleAndMaxLengthDescriptionAreGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + } + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldUpdateDescription() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasDescription(maxLengthDescription); + } + } + } + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java new file mode 100644 index 0000000..a0dd3eb --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java @@ -0,0 +1,691 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoControllerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + + private static final String ERROR_MESSAGE_KEY_MISSING_TITLE = "NotEmpty.todoDTO.title"; + private static final String ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION = "Size.todoDTO.description"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_TITLE = "Size.todoDTO.title"; + + private static final Long ID = 1L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private static final String TITLE = "title"; + + private MockMvc mockMvc; + + private TodoCrudService crudService; + + private StaticMessageSource messageSource; + + @Before + public void setUp() { + crudService = mock(TodoCrudService.class); + + messageSource = new StaticMessageSource(); + messageSource.setUseCodeAsDefaultMessage(true); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(crudService)) + .setHandlerExceptionResolvers(WebTestConfig.restErrorHandler(messageSource)) + .setLocaleResolver(WebTestConfig.fixedLocaleResolver(CURRENT_LOCALE)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setValidator(WebTestConfig.validator()) + .build(); + } + + public class Create { + + public class WhenTodoEntryIsNotValid { + + public class WhenTodoEntryIsEmpty { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + private TodoDTO newTodoEntry; + + @Before + public void createInputAndReturnNewTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + newTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .title(maxLengthTitle) + .build(); + + TodoDTO created = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.create(isA(TodoDTO.class))).willReturn(created); + } + + @Test + public void shouldReturnResponseStatusCreated() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isCreated()); + } + + @Test + public void shouldReturnCreatedTodoEntryAsJson() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldCreateNewTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verify(crudService, times(1)).create( + assertArg(created -> assertThatTodoDTO(created) + .hasDescription(maxLengthDescription) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoId() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwNotFoundException() { + given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnDeletedTodoEntry() { + TodoDTO deleted = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.delete(ID)).willReturn(deleted); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class FindAll { + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnNoTodoEntries() { + given(crudService.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } + + public class FindById { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findById(ID)).willReturn(found); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + public class WhenTodoEntryIsNotValid { + + public class WhenTitleAndDescriptionAreMissing { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + TodoDTO updatedTodoEntry; + + @Before + public void createInputAndReturnUpdatedTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + updatedTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .id(ID) + .title(maxLengthTitle) + .build(); + + TodoDTO updated = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.update(isA(TodoDTO.class))).willReturn(updated); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldUpdateTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verify(crudService, times(1)).update( + assertArg(updated -> assertThatTodoDTO(updated) + .hasDescription(maxLengthDescription) + .hasId(ID) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java new file mode 100644 index 0000000..b18d940 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java @@ -0,0 +1,250 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoSearchControllerTest { + + private MockMvc mockMvc; + + private TodoSearchService searchService; + + @Before + public void setUp() { + searchService = mock(TodoSearchService.class); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoSearchController(searchService)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setCustomArgumentResolvers(WebTestConfig.pageRequestArgumentResolver()) + .build(); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final String PAGE_NUMBER_STRING = "1"; + private final int PAGE_SIZE = 5; + private final String PAGE_SIZE_STRING = "5"; + private final String SEARCH_TERM = "itl"; + + private Pageable pageRequest; + + @Before + public void setUp() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnPageNumberAndPageSizeAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.number", is(PAGE_NUMBER))) + .andExpect(jsonPath("$.size", is(PAGE_SIZE))); + } + + @Test + public void shouldReturnSortInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.sort[*].direction[0]", is(WebTestConstants.SORT_DIRECTION_ASC))) + .andExpect(jsonPath("$.sort[*].property[0]", is(WebTestConstants.FIELD_NAME_TITLE))); + } + + @Test + public void shouldReturnPTotalElementInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + @Test + public void shouldPassSearchTermForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class)); + } + + @Test + public void shouldPassPageSizeAndNumberForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> { + assertThat(pageRequest.getPageNumber()).isEqualTo(PAGE_NUMBER); + assertThat(pageRequest.getPageSize()).isEqualTo(PAGE_SIZE); + } + )); + } + + @Test + public void shouldPassSortForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> assertThat( + pageRequest.getSort().getOrderFor(WebTestConstants.FIELD_NAME_TITLE).getDirection()) + .isEqualTo(Sort.Direction.ASC) + ) + ); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnEmptyPage() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements", is(0))); + } + } + + + public class WhenOneTodoEntryIsFound { + + private final Long ID= 1L; + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page pageWithOneTodoEntry = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(pageWithOneTodoEntry); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$.content[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TITLE))); + } + } + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java new file mode 100644 index 0000000..8578c38 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java @@ -0,0 +1,122 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import net.petrikainulainen.springdata.jpa.web.error.RestErrorHandler; +import org.springframework.context.MessageSource; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.FixedLocaleResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Locale; + +/** + * This factory class provides methods that can be used to create objects that are useful + * when we are writing unit tests for our controller methods by using the Spring MVC Test + * framework. + * + * @author Petri Kainulainen + */ +final class WebTestConfig { + + private WebTestConfig() {} + + /** + * Configures a {@link org.springframework.web.servlet.LocaleResolver} that always returns the + * configured {@link java.util.Locale}. + * + * @return + */ + static LocaleResolver fixedLocaleResolver(Locale fixedLocale) { + return new FixedLocaleResolver(fixedLocale); + } + + /** + * This method creates a custom {@link org.springframework.http.converter.HttpMessageConverter} which ensures that: + * + *
    + *
  • Null values are ignored.
  • + *
  • + * The new Java 8 date objects are serialized in standard + * ISO-8601 string representation. + *
  • + *
+ * + * @return + */ + static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return converter; + } + + /** + * This method ensures that the {@link RestErrorHandler} class + * is used to handle the exceptions thrown by the tested controller. I borrowed this idea from + * this StackOverflow answer. + * + * @return an error handler component that delegates relevant exceptions forward to the {@link RestErrorHandler} class. + */ + static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) { + final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() { + @Override + protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerMethod handlerMethod, + final Exception exception) { + Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception); + if (method != null) { + return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method); + } + return super.getExceptionHandlerMethod(handlerMethod, exception); + } + }; + exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter())); + exceptionResolver.afterPropertiesSet(); + return exceptionResolver; + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Sort} objects by using the request params of the + * incoming request. + * @return + */ + static SortHandlerMethodArgumentResolver sortArgumentResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Pageable} objects by using the request params of the + * incoming request. + * @return + */ + static PageableHandlerMethodArgumentResolver pageRequestArgumentResolver() { + return new PageableHandlerMethodArgumentResolver(sortArgumentResolver()); + } + + /** + * This method creates a validator object that adds support for bean validation API 1.0 and 1.1. + * + * @return The created validator object. + */ + static LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java new file mode 100644 index 0000000..1bf538d --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java @@ -0,0 +1,37 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.http.MediaType; + +import java.nio.charset.Charset; + +/** + * @author Petri Kainulainen + */ +public final class WebTestConstants { + + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), + MediaType.APPLICATION_JSON.getSubtype(), + Charset.forName("utf8") + ); + + static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + static final String ERROR_CODE_VALIDATION_FAILED = "BAD_REQUEST"; + + static final String FIELD_NAME_DESCRIPTION = "description"; + static final String FIELD_NAME_TITLE = "title"; + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + static final String REQUEST_PARAM_PAGE_NUMBER = "page"; + static final String REQUEST_PARAM_PAGE_SIZE = "size"; + static final String REQUEST_PARAM_SEARCH_TERM = "searchTerm"; + static final String REQUEST_PARAM_SORT = "sort"; + + static final String SORT_DIRECTION_ASC = "ASC"; + + /** + * Prevents instantiation. + */ + private WebTestConstants() {} +} diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java new file mode 100644 index 0000000..9340fe6 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +/** + * @author Petri Kainulainen + */ +final class WebTestUtil { + + /** + * Prevents instantiation + */ + private WebTestUtil() {} + + /** + * Transforms an object into JSON and returns the JSON as a byte array. + * @param object The object that is transformed into JSON. + * @return The JSON representation of an object as a byte array. + * @throws IOException + */ + static byte[] convertObjectToJsonBytes(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsBytes(object); + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ErrorDTOTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java similarity index 93% rename from query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ErrorDTOTest.java rename to criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java index f8cb260..528a552 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ErrorDTOTest.java +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java @@ -1,6 +1,7 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.ErrorDTO; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTOTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java similarity index 93% rename from query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTOTest.java rename to criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java index fbf7f74..25fc6bf 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/FieldErrorDTOTest.java +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java @@ -1,6 +1,7 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandlerTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java similarity index 73% rename from query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandlerTest.java rename to criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java index bf745a6..5ff8f34 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/RestErrorHandlerTest.java +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java @@ -1,4 +1,4 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import com.nitorcreations.junit.runners.NestedRunner; import net.petrikainulainen.springdata.jpa.todo.TodoDTO; @@ -6,7 +6,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.core.MethodParameter; @@ -18,6 +17,7 @@ import java.util.List; import java.util.Locale; +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.eq; @@ -53,13 +53,16 @@ public class HandleTodoEntryNotFound { private static final String ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; private static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 99"; - @Test - public void shouldFindErrorMessageByUsingCurrentLocale() { + @Before + public void returnErrorMessageNotFound() { given(messageSource.getMessage( isA(MessageSourceResolvable.class), isA(Locale.class)) ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + @Test + public void shouldFindErrorMessageByUsingCurrentLocale() { errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); verify(messageSource, times(1)).getMessage(isA(MessageSourceResolvable.class), eq(CURRENT_LOCALE)); @@ -67,45 +70,30 @@ public void shouldFindErrorMessageByUsingCurrentLocale() { @Test public void shouldFindErrorMessageByUsingCorrectId() { - given(messageSource.getMessage( - isA(MessageSourceResolvable.class), - isA(Locale.class)) - ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); - errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); - ArgumentCaptor messageRequestArgument = ArgumentCaptor.forClass(MessageSourceResolvable.class); - verify(messageSource, times(1)).getMessage(messageRequestArgument.capture(), eq(CURRENT_LOCALE)); - - MessageSourceResolvable messageRequest = messageRequestArgument.getValue(); - assertThat(messageRequest.getArguments()) - .containsOnly(TODO_ID); + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getArguments()) + .containsOnly(TODO_ID) + ), + eq(CURRENT_LOCALE) + ); } @Test public void shouldFindErrorMessageByUsingCorrectMessageCode() { - given(messageSource.getMessage( - isA(MessageSourceResolvable.class), - isA(Locale.class)) - ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); - errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); - ArgumentCaptor messageRequestArgument = ArgumentCaptor.forClass(MessageSourceResolvable.class); - verify(messageSource, times(1)).getMessage(messageRequestArgument.capture(), eq(CURRENT_LOCALE)); - - MessageSourceResolvable messageRequest = messageRequestArgument.getValue(); - assertThat(messageRequest.getCodes()) - .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND); + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getCodes()) + .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND) + ), + eq(CURRENT_LOCALE) + ); } @Test public void shouldReturnErrorThatHasCorrectErrorCode() { - given(messageSource.getMessage( - isA(MessageSourceResolvable.class), - isA(Locale.class) - )).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); - ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); assertThat(error.getCode()).isEqualTo(ERROR_CODE_TODO_ENTRY_NOT_FOUND); @@ -113,11 +101,6 @@ public void shouldReturnErrorThatHasCorrectErrorCode() { @Test public void shouldReturnErrorThatHasCorrectMessage() { - given(messageSource.getMessage( - isA(MessageSourceResolvable.class), - isA(Locale.class) - )).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); - ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); assertThat(error.getMessage()).isEqualTo(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); @@ -140,17 +123,21 @@ public class WhenOneValidationErrorIsFound { public class WhenMessageIsFound { - @Test - public void shouldReturnErrorThatHasCorrectCode() { + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnErrorMessage() { FieldError fieldError = new FieldErrorBuilder() .defaultMessage(FIELD_DEFAULT_MESSAGE) .fieldName(FIELD_WITH_VALIDATION_ERROR) .build(); - - MethodArgumentNotValidException ex = createExceptionWithFieldErrors(fieldError); - given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); @@ -158,15 +145,6 @@ public void shouldReturnErrorThatHasCorrectCode() { @Test public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { - FieldError fieldError = new FieldErrorBuilder() - .defaultMessage(FIELD_DEFAULT_MESSAGE) - .fieldName(FIELD_WITH_VALIDATION_ERROR) - .build(); - - MethodArgumentNotValidException ex = createExceptionWithFieldErrors(fieldError); - - given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); - ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); List fieldErrors = validationErrors.getFieldErrors(); @@ -180,18 +158,22 @@ public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { public class WhenMessageIsNotFound { - @Test - public void shouldReturnErrorThatHasCorrectCode() { + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnDefaultErrorMessage() { FieldError fieldError = new FieldErrorBuilder() .defaultMessage(FIELD_DEFAULT_MESSAGE) .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) .fieldName(FIELD_WITH_VALIDATION_ERROR) .build(); - - MethodArgumentNotValidException ex = createExceptionWithFieldErrors(fieldError); - given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); @@ -199,16 +181,6 @@ public void shouldReturnErrorThatHasCorrectCode() { @Test public void shouldReturnErrorThatHasFieldErrorWithMostAccurateFieldErrorCode() { - FieldError fieldError = new FieldErrorBuilder() - .defaultMessage(FIELD_DEFAULT_MESSAGE) - .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) - .fieldName(FIELD_WITH_VALIDATION_ERROR) - .build(); - - MethodArgumentNotValidException ex = createExceptionWithFieldErrors(fieldError); - - given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); - ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); List fieldErrors = validationErrors.getFieldErrors(); diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTOTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java similarity index 87% rename from query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTOTest.java rename to criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java index 4df800f..8ae069a 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/ValidationErrorDTOTest.java +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java @@ -1,13 +1,15 @@ -package net.petrikainulainen.springdata.jpa.web; +package net.petrikainulainen.springdata.jpa.web.error; import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import net.petrikainulainen.springdata.jpa.web.error.ValidationErrorDTO; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; -import static net.petrikainulainen.springdata.jpa.common.ThrowableCaptor.thrown; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; /** * @author Petri Kainulainen @@ -34,7 +36,7 @@ public void shouldThrowException() { public void shouldNotCreateNewFieldError() { ValidationErrorDTO validationErrors = new ValidationErrorDTO(); - thrown(() -> validationErrors.addFieldError(null, MESSAGE)); + catchThrowable(() -> validationErrors.addFieldError(null, MESSAGE)); assertThat(validationErrors.getFieldErrors()).isEmpty(); } @@ -52,7 +54,7 @@ public void shouldThrowException() { public void shouldNotCreateNewFieldError() { ValidationErrorDTO validationErrors = new ValidationErrorDTO(); - thrown(() -> validationErrors.addFieldError("", MESSAGE)); + catchThrowable(() -> validationErrors.addFieldError("", MESSAGE)); assertThat(validationErrors.getFieldErrors()).isEmpty(); } @@ -73,7 +75,7 @@ public void shouldThrowException() { public void shouldNotCreateNewFieldError() { ValidationErrorDTO validationErrors = new ValidationErrorDTO(); - thrown(() -> validationErrors.addFieldError(FIELD, null)); + catchThrowable(() -> validationErrors.addFieldError(FIELD, null)); assertThat(validationErrors.getFieldErrors()).isEmpty(); } @@ -91,7 +93,7 @@ public void shouldThrowException() { public void shouldNotCreateNewFieldError() { ValidationErrorDTO validationErrors = new ValidationErrorDTO(); - thrown(() -> validationErrors.addFieldError(FIELD, "")); + catchThrowable(() -> validationErrors.addFieldError(FIELD, "")); assertThat(validationErrors.getFieldErrors()).isEmpty(); } diff --git a/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java new file mode 100644 index 0000000..1928461 --- /dev/null +++ b/criteria-api/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class UserDTOTest { + + public class CreateNew { + + private final String ROLE_USER = UserRole.ROLE_USER.name(); + + public class WhenUsernameIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER); + new UserDTO("", authorities); + } + } + + public class WhenUserNameIsNotEmpty { + + private final String USERNAME = "username"; + + public class WhenUserHasNoGrantedAuthorities { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + new UserDTO(USERNAME, new ArrayList<>()); + } + } + + public class WhenUserHasOneGrantedAuthority { + + public class WhenGrantedAuthorityIsKnown { + + private Collection authorities; + + @Before + public void createKnownAuthority() { + authorities = createAuthorities(ROLE_USER); + } + + @Test + public void shouldSetUsername() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getUsername()).isEqualTo(USERNAME); + } + + @Test + public void shouldSetRole() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getRole()).isEqualTo(UserRole.ROLE_USER); + } + } + + public class WhenGrantedAuthorityIsUnknown { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities("UNKNOWN_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + + public class WhenUserHasMoreThanOneGrantedAuthority { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER, "ANOTHER_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + } + + private Collection createAuthorities(String... roles) { + List authorities = new ArrayList<>(); + + for (String role: roles) { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); + authorities.add(authority); + } + + return authorities; + } +} diff --git a/custom-method-all-repos/.gitignore b/custom-method-all-repos/.gitignore new file mode 100644 index 0000000..02895f1 --- /dev/null +++ b/custom-method-all-repos/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.gradle +.idea +*.iml +build +h2db +target +node_modules +bower_components +build \ No newline at end of file diff --git a/tutorial-part-five/LICENSE b/custom-method-all-repos/LICENSE similarity index 88% rename from tutorial-part-five/LICENSE rename to custom-method-all-repos/LICENSE index b333aa5..c2b516d 100644 --- a/tutorial-part-five/LICENSE +++ b/custom-method-all-repos/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011 Petri Kainulainen +Copyright 2015 Petri Kainulainen Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -10,4 +10,4 @@ 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. \ No newline at end of file +limitations under the License. diff --git a/custom-method-all-repos/README.md b/custom-method-all-repos/README.md new file mode 100644 index 0000000..82c34db --- /dev/null +++ b/custom-method-all-repos/README.md @@ -0,0 +1,70 @@ +This blog post is the example application of the following blog posts: + +* [Spring Data JPA Tutorial: Getting the Required Dependencies](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-getting-the-required-dependencies/) +* [Spring Data JPA Tutorial: Configuration](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/) +* [Spring Data JPA Tutorial: CRUD](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/) +* [Spring Data JPA Tutorial: Auditing, Part One](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-one/) +* [Spring Data JPA Tutorial: Auditing, Part Two](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-two/) +* [Spring Data JPA Tutorial: Adding Custom Methods Into All Repositories]() - Not published yet + +Prerequisites +============= + +You need to install the following tools if you want to run this application: + +Backend +--------- + +* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* [Maven](http://maven.apache.org/) (the application is tested with Maven 3.2.1) + +Frontend +---------- + +* [Node.js](http://nodejs.org/) +* [NPM](https://www.npmjs.org/) +* [Bower](http://bower.io/) +* [Gulp](http://gulpjs.com/) + +You can install these tools by following these steps: + +1. Install Node.js by using a [downloaded binary](http://nodejs.org/download/) or a [package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). + You can also read this blog post: [How to install Node.js and NPM](http://blog.nodeknockout.com/post/65463770933/how-to-install-node-js-and-npm) + +2. Install Bower by using the following command: + + npm install -g bower + +3. Install Gulp by using the following command: + + npm install -g gulp + + +Running the Tests +================= + +You can run the unit tests by using the following command: + + mvn clean test -P dev + +You can run the integration tests by using the following command: + + mvn clean verify -P integration-test + +Running the Application +======================= + +You can run the application by using the following command: + + mvn clean jetty:run -P dev + +Credits +========= + +* Kyösti Herrala. The Gulp build script and its Maven integration are based on Kyösti's ideas. +* [Techniques for authentication in AngularJS applications](https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec) + +Known Issues +============ + +* If you refresh the login page, you aren't redirected away from it after successful login. \ No newline at end of file diff --git a/custom-method-all-repos/frontend/.bowerrc b/custom-method-all-repos/frontend/.bowerrc new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/custom-method-all-repos/frontend/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file diff --git a/custom-method-all-repos/frontend/.jshintrc b/custom-method-all-repos/frontend/.jshintrc new file mode 100644 index 0000000..f648d46 --- /dev/null +++ b/custom-method-all-repos/frontend/.jshintrc @@ -0,0 +1,33 @@ +{ + "globalstrict": true, + "browser": true, + "devel": true, + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "globals": { + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "angular": true, + "jQuery": true, + "_": true, + "$": true + } +} \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/app.js b/custom-method-all-repos/frontend/app/app.js new file mode 100644 index 0000000..1840cd6 --- /dev/null +++ b/custom-method-all-repos/frontend/app/app.js @@ -0,0 +1,97 @@ +'use strict'; + +var App = angular.module('app', [ + 'angular-logger', + 'http-auth-interceptor', + 'ngLocale', + 'ngCookies', + 'ngResource', + 'ngSanitize', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.router', + 'ui.utils', + 'angular-growl', + 'angularMoment', + 'spring-security-csrf-token-interceptor', + + //Partials + 'templates', + + //Account + 'app.account.config', 'app.account.directives', 'app.account.controllers', 'app.account.services', + + //Common + 'app.common.config', 'app.common.controllers', 'app.common.directives', 'app.common.services', + + //Todo + 'app.todo.controllers', 'app.todo.directives', 'app.todo.services', + + //Search + 'app.search.controllers', 'app.search.directives', 'app.search.services' + +]); + +App.run(['$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', 'authService', 'AuthenticationService', 'COMMON_EVENTS', + function ($log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser, authService, AuthenticationService, COMMON_EVENTS) { + + var logger = $log.getInstance('app'); + + //This function retries all requests that were failed because of + //the 401 response. + function listenAuthenticationEvents() { + var confirmLogin = function() { + authService.loginConfirmed(); + }; + + $rootScope.$on(AUTH_EVENTS.loginSuccess, confirmLogin); + + var viewLogInPage = function() { + logger.info('User is not authenticated. Rendering login view.'); + $state.go('todo.login'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthenticated, viewLogInPage); + + var viewTodoListPage = function() { + logger.info("User logged out. REndering todo list view."); + $state.go('todo.list', {}, {reload: true}); + }; + + $rootScope.$on(AUTH_EVENTS.logoutSuccess, viewTodoListPage); + + var viewForbiddenPage = function() { + logger.info('Permission was denied for user: %j', AuthenticatedUser); + $state.go('todo.forbidden'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthorized, viewForbiddenPage); + } + + function listenCommonEvents() { + + var view404Page = function() { + logger.info('Requested page was not found.'); + $state.go('todo.404'); + }; + + $rootScope.$on(COMMON_EVENTS.notFound, view404Page); + } + + //This function ensures that anonymous users cannot access states + //that marked as protected (i.e. the value of the authenticated + //property is set to true). + function secureProtectedStates() { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + logger.trace('Moving to state: %s', toState.name); + AuthenticationService.authorizeStateChange(event, toState, toParams); + }); + } + + $rootScope.currentUser = AuthenticatedUser; + + listenAuthenticationEvents(); + listenCommonEvents(); + secureProtectedStates(); +}]); + diff --git a/custom-method-all-repos/frontend/app/assets/i18n/en.json b/custom-method-all-repos/frontend/app/assets/i18n/en.json new file mode 100644 index 0000000..71d4d0c --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/i18n/en.json @@ -0,0 +1,96 @@ +{ + "app.title.label": "Spring Data JPA Tutorial - Query Methods", + "dialogs": { + "delete.dialog": { + "cancel.button.label": "Cancel", + "delete.button.label": "Delete", + "text": "Are you sure that you want to delete the todo entry with title: {{title}}?", + "title": "Delete todo entry?" + } + }, + "directives": { + "login.form": { + "login.button": "Login", + "login.failed": "Login failed!" + }, + "log.out.link.label": "Log Out", + "todo.form": { + "cancel.button": "Cancel", + "save.button": "Save" + } + }, + "footer.message": "Spring Data JPA example application by Petri Kainulainen", + "header.brand.label": "Spring Data JPA Tutorial", + "pages": { + "add.page": { + "title": "Add new todo entry", + "link.label": "Add new todo entry" + }, + "delete.link": "Delete", + "edit.page": { + "link.label": "Edit", + "title": "Edit todo entry" + }, + "forbidden.page": { + "text": "Permission denied.", + "title": "Forbidden" + }, + "not.found.page": { + "text": "The page that you were looking for was not found.", + "title": "Not Found" + }, + "list.page": { + "title": "Things to do", + "texts": { + "no.todo.entries.found": "Nothing to do (yet)." + } + }, + "login.page": { + "title": "Log In" + }, + "search.results.page.title": "Search Results", + "view.page": { + "title": "View Todo Entry" + } + }, + "login": { + "help": "Log in by using username: 'user' and password: 'password'", + "username": "Username", + "username.placeholder": "Enter username", + "password": "Password", + "password.placeholder": "Enter password" + }, + "search": { + "term.field.placeholder": "Search", + "missing.characters.text": "{{missingCharCount}} characters missing" + }, + "todo": { + "created.by.prefix": "by", + "creation.time": "Created at", + "description": "Description", + "description.placeholder": "Enter description", + "messages": { + "description.maxLength": "Description cannot be longer than 500 characters", + "title.maxLength": "Title cannot be longer than 100 characters", + "title.required": "Title is required" + }, + "modified.by.prefix": "by", + "modification.time": "Modified at", + "notifications": { + "add": { + "error": "Adding a new todo entry failed.", + "success": "A new todo entry was added." + }, + "delete": { + "error": "Deleting the todo entry failed.", + "success": "Deleted the todo entry." + }, + "edit": { + "error": "Updating the information of a todo entry failed.", + "success": "Updated the information of the todo entry." + } + }, + "title": "Title", + "title.placeholder": "Enter title" + } +} \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/account/forbidden-view.html b/custom-method-all-repos/frontend/app/assets/partials/account/forbidden-view.html new file mode 100644 index 0000000..c761f3e --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/account/forbidden-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/account/login-form-directive.html b/custom-method-all-repos/frontend/app/assets/partials/account/login-form-directive.html new file mode 100644 index 0000000..d2f14aa --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/account/login-form-directive.html @@ -0,0 +1,36 @@ +
+ + +
+ +
+
+ : + +
+
+ : + +
+
+ +
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/account/login-view.html b/custom-method-all-repos/frontend/app/assets/partials/account/login-view.html new file mode 100644 index 0000000..199d339 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/account/login-view.html @@ -0,0 +1,6 @@ +

+ +
+
+

+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/account/logout-link-directive.html b/custom-method-all-repos/frontend/app/assets/partials/account/logout-link-directive.html new file mode 100644 index 0000000..4d9550a --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/account/logout-link-directive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/common/not-found-view.html b/custom-method-all-repos/frontend/app/assets/partials/common/not-found-view.html new file mode 100644 index 0000000..7edf553 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/common/not-found-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/search/search-form-directive.html b/custom-method-all-repos/frontend/app/assets/partials/search/search-form-directive.html new file mode 100644 index 0000000..674143e --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/search/search-form-directive.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/search/search-result-view.html b/custom-method-all-repos/frontend/app/assets/partials/search/search-result-view.html new file mode 100644 index 0000000..e199e19 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/search/search-result-view.html @@ -0,0 +1,6 @@ +
+

+
+ +
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/add-todo-view.html b/custom-method-all-repos/frontend/app/assets/partials/todo/add-todo-view.html new file mode 100644 index 0000000..0a0406a --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/add-todo-view.html @@ -0,0 +1,9 @@ +

+ +
+
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/delete-todo-modal.html b/custom-method-all-repos/frontend/app/assets/partials/todo/delete-todo-modal.html new file mode 100644 index 0000000..b390319 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/delete-todo-modal.html @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/edit-todo-view.html b/custom-method-all-repos/frontend/app/assets/partials/todo/edit-todo-view.html new file mode 100644 index 0000000..1695ae6 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/edit-todo-view.html @@ -0,0 +1,8 @@ +

+
+
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/todo-form-directive.html b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-form-directive.html new file mode 100644 index 0000000..c7815d0 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-form-directive.html @@ -0,0 +1,52 @@ +
+
+ : + +
+ + +
+
+
+ : + +
+ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-directive.html b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-directive.html new file mode 100644 index 0000000..60ed955 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-directive.html @@ -0,0 +1,8 @@ +
+

+
+ diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-view.html b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-view.html new file mode 100644 index 0000000..6a83ba4 --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/todo-list-view.html @@ -0,0 +1,7 @@ +
+

+ +
+ +
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/assets/partials/todo/view-todo-view.html b/custom-method-all-repos/frontend/app/assets/partials/todo/view-todo-view.html new file mode 100644 index 0000000..374c16d --- /dev/null +++ b/custom-method-all-repos/frontend/app/assets/partials/todo/view-todo-view.html @@ -0,0 +1,25 @@ +
+

+ +
+

{{todoEntry.title}}

+

{{todoEntry.description}}

+
+

+ + {{"todo.creation.time" | translate}}: {{todoEntry.creationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.created.by.prefix" | translate}} {{todoEntry.createdByUser}} + {{"todo.modification.time" | translate }}: {{todoEntry.modificationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.modified.by.prefix" | translate}} {{todoEntry.modifiedByUser}} + +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/account/account.config.js b/custom-method-all-repos/frontend/app/module/account/account.config.js new file mode 100644 index 0000000..c689bb1 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/account/account.config.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('app.account.config', []) + .constant('AUTH_EVENTS', { + loginSuccess: 'event:auth-login-success', + loginFailed: 'event:auth-login-failed', + logoutSuccess: 'event:auth-logout-success', + sessionTimeout: 'event:auth-session-timeout', + notAuthenticated: 'event:auth-loginRequired', + notAuthorized: 'event:auth-forbidden' + }) + .config(['csrfProvider', function(csrfProvider) { + // optional configurations + csrfProvider.config({ + httpTypes: ['PUT', 'POST', 'DELETE'], + maxRetries: 1, + url: '/api/csrf' + }); + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/account/account.controllers.js b/custom-method-all-repos/frontend/app/module/account/account.controllers.js new file mode 100644 index 0000000..79f6fbe --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/account/account.controllers.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('app.account.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.login', { + url: 'login', + controller: 'LoginController', + templateUrl: 'account/login-view.html' + }) + .state('todo.forbidden', { + url: 'forbidden', + controller: 'ForbiddenController', + templateUrl: 'account/forbidden-view.html' + }); + } + ]) + .controller('ForbiddenController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.ForbiddenController'); + logger.info("Rendering forbidden view."); + }]) + .controller('LoginController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.LoginController'); + logger.info('Rendering login form.'); + }]); + diff --git a/custom-method-all-repos/frontend/app/module/account/account.directives.js b/custom-method-all-repos/frontend/app/module/account/account.directives.js new file mode 100644 index 0000000..2ff0aa6 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/account/account.directives.js @@ -0,0 +1,44 @@ +'use strict'; + +angular.module('app.account.directives', []) + .directive('logOutLink', ['$log', 'AuthenticationService', function ($log, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.logOutLink'); + + return { + link: function (scope, element, attr) { + scope.logOut = function() { + logger.info('Logging user out.'); + AuthenticationService.logOut(); + }; + }, + templateUrl: 'account/logout-link-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('loginForm', ['$log', 'AUTH_EVENTS', 'AuthenticationService', function ($log, AUTH_EVENTS, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.loginForm'); + + return { + link: function (scope, element, attr) { + scope.login = {}; + scope.loginFailed = false; + + scope.$on(AUTH_EVENTS.loginFailed, function() { + logger.info('Received login failed event.'); + scope.loginFailed = true; + }); + + scope.submitLoginForm = function() { + logger.info('Submitting log in form.'); + AuthenticationService.logIn(scope.login.username, scope.login.password); + }; + }, + templateUrl: 'account/login-form-directive.html', + scope: { + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/account/account.services.js b/custom-method-all-repos/frontend/app/module/account/account.services.js new file mode 100644 index 0000000..7cbd82d --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/account/account.services.js @@ -0,0 +1,79 @@ +'use strict'; + +angular.module('app.account.services', ['ngResource']) + .service('AuthenticatedUser', function () { + this.create = function (username, role) { + this.username = username; + this.role = role; + }; + this.destroy = function () { + this.username = null; + this.role = null; + }; + }) + .factory('AuthenticationService', ['$http', '$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', + function($http, $log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser) { + + var logger = $log.getInstance('app.account.services.AuthenticationService'); + + return { + authorizeStateChange: function(event, toState, toParams) { + logger.debug('Authorizing state change to state: %s', toState.name); + if (toState.authenticate && !this.isAuthenticated()) { + event.preventDefault(); + + logger.debug('Authentication is not found. Fetching it from the backend.'); + var self = this; + $http.get('/api/authenticated-user').success(function(user) { + logger.debug('Found authenticated user: %j', user); + AuthenticatedUser.create(user.username, user.role); + + if (!self.isAuthenticated) { + logger.debug('Unauthenticated users is: %j', AuthenticatedUser); + $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); + } + else { + logger.debug('User is authenticated. Continuing to the target state: %s', toState.name); + $state.go(toState.name, toParams); + } + }); + } + }, + isAuthenticated: function() { + logger.debug('Checking if user: %j is authenticated.', AuthenticatedUser); + return AuthenticatedUser.username; + }, + logIn: function(username, password) { + logger.info('Logging in user with username: %s', username); + + var transform = function(data){ + return $.param(data); + }; + + $http.post('/api/login', {username: username, password: password}, { + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + ignoreAuthModule: true, + transformRequest: transform + }) + .success(function(user) { + logger.info('Login successful for user: %j', user); + AuthenticatedUser.create(user.username, user.role); + $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); + }) + .error(function() { + logger.info('Login failed'); + $rootScope.$broadcast(AUTH_EVENTS.loginFailed); + }); + }, + logOut: function() { + if (this.isAuthenticated()) { + $http.post('/api/logout', {}) + .success(function() { + logger.info('User is logged out.'); + AuthenticatedUser.destroy(); + $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); + }); + } + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/common/common.config.js b/custom-method-all-repos/frontend/app/module/common/common.config.js new file mode 100644 index 0000000..6ffca02 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/common/common.config.js @@ -0,0 +1,60 @@ +'use strict'; + +angular.module('app.common.config', []) + .constant('COMMON_EVENTS', { + notFound: 'event:not-found' + }) + .config(['logEnhancerProvider', function (logEnhancerProvider) { + logEnhancerProvider.datetimePattern = 'DD.MM.YYYY HH:mm:ss'; + logEnhancerProvider.prefixPattern = '%s::[%s]> '; + logEnhancerProvider.logLevels = { + '*': logEnhancerProvider.LEVEL.OFF + }; + }]) + .config(['$urlRouterProvider', '$locationProvider', + function ($urlRouterProvider, $locationProvider) { + //this prevents infinite $digest loop when we invoke the + //preventDefault() method in $stateChangeStart event handler. + //See: https://github.com/angular-ui/ui-router/issues/600#issuecomment-47228922 + $urlRouterProvider.otherwise( function($injector, $location) { + var $state = $injector.get("$state"); + $state.go("todo.list"); + }); + + // Without server side support html5 must be disabled. + $locationProvider.html5Mode(false); + } + ]) + .config(['$translateProvider', function ($translateProvider) { + // Initialize angular-translate + $translateProvider.useStaticFilesLoader({ + prefix: '/i18n/', + suffix: '.json' + }); + + $translateProvider.preferredLanguage('en'); + $translateProvider.useSanitizeValueStrategy('escaped'); + $translateProvider.useLocalStorage(); + $translateProvider.useMissingTranslationHandlerLog(); + }]) + .config(['growlProvider', function (growlProvider) { + growlProvider.globalTimeToLive(5000); + }]) + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push([ + '$injector', + function ($injector) { + return $injector.get('404Interceptor'); + } + ]); + }]) + .factory('404Interceptor', ['$rootScope', '$q', 'COMMON_EVENTS', function ($rootScope, $q, COMMON_EVENTS) { + return { + responseError: function(response) { + if (response.status === 404) { + $rootScope.$broadcast(COMMON_EVENTS.notFound); + } + return $q.reject(response); + } + }; + }]); diff --git a/custom-method-all-repos/frontend/app/module/common/common.controllers.js b/custom-method-all-repos/frontend/app/module/common/common.controllers.js new file mode 100644 index 0000000..811f15e --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/common/common.controllers.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('app.common.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.404', { + url: 'not-found', + controller: 'NotFoundController', + templateUrl: 'common/not-found-view.html' + }); + } + ]) + .controller('NotFoundController', ['$log', function($log) { + var logger = $log.getInstance('app.common.controllers.NotFoundController'); + logger.info("Rendering 404 view."); + }]); + diff --git a/custom-method-all-repos/frontend/app/module/common/common.directives.js b/custom-method-all-repos/frontend/app/module/common/common.directives.js new file mode 100644 index 0000000..7c56027 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/common/common.directives.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('app.common.directives', []) + .directive('staticInclude', ['$http', '$templateCache', '$compile', function ($http, $templateCache, $compile) { + return function(scope, element, attrs) { + var templatePath = attrs.staticInclude; + + $http.get(templatePath, {cache: $templateCache}).success(function (response) { + var contents = $('
').html(response).contents(); + element.html(contents); + $compile(contents)(scope); + }); + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/common/common.services.js b/custom-method-all-repos/frontend/app/module/common/common.services.js new file mode 100644 index 0000000..de9d0e6 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/common/common.services.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('app.common.services', []) + .service('NotificationService', ['$rootScope', 'growl', function ($rootScope, growl) { + var flashMessageQueue = []; + + function displayNotification(message, type) { + if (type === 'success') { + growl.success(message); + } else if (type === 'warn') { + growl.warning(message); + } else if (type === 'info') { + growl.info(message); + } else { + growl.error(message); + } + } + + // Display all flash notifications after state has changed + $rootScope.$on("$stateChangeSuccess", function () { + while (flashMessageQueue.length > 0) { + var item = flashMessageQueue.shift(); + if (item) { + displayNotification(item.message, item.type); + } + } + }); + + // Public API + return { + 'flashMessage': function (message, type) { + flashMessageQueue.push({message: message, type: type || 'info'}); + } + }; + }]); diff --git a/custom-method-all-repos/frontend/app/module/search/search.controllers.js b/custom-method-all-repos/frontend/app/module/search/search.controllers.js new file mode 100644 index 0000000..f400438 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/search/search.controllers.js @@ -0,0 +1,30 @@ +'use strict'; + +angular.module('app.search.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.search', { + authenticate: true, + url: 'todo/search/:searchTerm', + controller: 'SearchResultController', + templateUrl: 'search/search-result-view.html', + resolve: { + searchResults: ['TodoSearchService', '$stateParams', function(TodoSearchService, $stateParams) { + if ($stateParams.searchTerm) { + return TodoSearchService.findBySearchTerm($stateParams.searchTerm); + } + + return null; + }] + } + }); + } + ]) + .controller('SearchResultController', ['$log', '$scope', 'searchResults', + function($log, $scope, searchResults) { + var logger = $log.getInstance('app.search.controllers.SearchResultController'); + logger.info('Rendering search results page.'); + $scope.todoEntries = searchResults; + }]); + diff --git a/custom-method-all-repos/frontend/app/module/search/search.directives.js b/custom-method-all-repos/frontend/app/module/search/search.directives.js new file mode 100644 index 0000000..fa7be29 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/search/search.directives.js @@ -0,0 +1,66 @@ +'use strict'; + +angular.module('app.search.directives', []) + .directive('searchForm', ['$log', '$state', function($log, $state) { + + var logger = $log.getInstance('app.search.directives.searchForm'); + + return { + link: function (scope, element, attr) { + var userWritingSearchTerm = false; + var minimumSearchTermLength = 3; + + scope.translationData = { + missingCharCount: minimumSearchTermLength + }; + + scope.search = {}; + scope.search.searchTerm = ""; + + scope.searchFieldBlur = function() { + userWritingSearchTerm = false; + scope.search.searchTerm = ""; + scope.translationData.missingCharCount = minimumSearchTermLength; + }; + + scope.searchFieldFocus = function() { + userWritingSearchTerm = true; + }; + + scope.showMissingCharacterText = function() { + if (!scope.search.searchTerm) { + scope.search.searchTerm = ""; + } + + if (userWritingSearchTerm) { + if (scope.search.searchTerm.length < minimumSearchTermLength) { + return true; + } + } + + return false; + }; + + scope.search = function() { + logger.trace('User is using the search term: %s', scope.search.searchTerm); + + if (scope.search.searchTerm.length < minimumSearchTermLength) { + scope.translationData.missingCharCount = minimumSearchTermLength - scope.search.searchTerm.length; + logger.trace('%s characters are missing. Search is not invoked.', scope.translationData.missingCharCount); + } + else { + scope.translationData.missingCharCount = 0; + $state.go('todo.search', + {searchTerm: scope.search.searchTerm}, + {reload: true, inherit: true, notify: true} + ); + } + }; + + }, + templateUrl: 'search/search-form-directive.html', + scope: { + currentUser: '=' + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/search/search.services.js b/custom-method-all-repos/frontend/app/module/search/search.services.js new file mode 100644 index 0000000..2c18447 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/search/search.services.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('app.search.services', ['ngResource']) + .factory('TodoSearchService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/search', {}, { + 'query': {method:'GET', isArray:true} + }); + + var logger = $log.getInstance('app.search.services.TodoSearchService'); + + return { + findBySearchTerm: function(searchTerm) { + logger.info('Searching todo entries with search term: %s', searchTerm); + return api.query({searchTerm: searchTerm}); + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/todo/todo.controllers.js b/custom-method-all-repos/frontend/app/module/todo/todo.controllers.js new file mode 100644 index 0000000..d4da7bf --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/todo/todo.controllers.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('app.todo.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo', { + url: '/', + abstract: true, + template: '' + }) + .state('todo.add', { + authenticate: true, + url: 'todo/add', + controller: 'AddTodoController', + templateUrl: 'todo/add-todo-view.html' + }) + .state('todo.edit', { + authenticate: true, + url: 'todo/:id/edit', + controller: 'EditTodoController', + templateUrl: 'todo/edit-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }) + .state('todo.list', { + authenticate: true, + url: '', + controller: 'TodoListController', + templateUrl: 'todo/todo-list-view.html', + resolve: { + todoEntries: ['TodoService', function(TodoService) { + return TodoService.findAll(); + }] + } + }) + .state('todo.view', { + authenticate: true, + url: 'todo/:id', + controller: 'ViewTodoController', + templateUrl: 'todo/view-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }); + } + ]) + .controller('AddTodoController', ['$log', '$scope', function($log, $scope) { + var logger = $log.getInstance('app.todo.controllers.AddTodoController'); + logger.info('Rendering add todo entry page.'); + $scope.todoEntry = {}; + }]) + .controller('EditTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.EditTodoController'); + logger.info('Rendering edit todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]) + .controller('TodoListController', ['$log', '$scope', 'todoEntries', function($log, $scope, todoEntries) { + var logger = $log.getInstance('app.todo.controllers.TodoListController'); + logger.info('Rendering todo entry list page for %s todo entries.', todoEntries.length); + $scope.todoEntries = todoEntries; + }]) + .controller('ViewTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.ViewTodoController'); + logger.info('Rendering view todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/todo/todo.directives.js b/custom-method-all-repos/frontend/app/module/todo/todo.directives.js new file mode 100644 index 0000000..a2377b5 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/todo/todo.directives.js @@ -0,0 +1,102 @@ +'use strict'; + +angular.module('app.todo.directives', []) + .controller('DeleteTodoController', ['$log', '$scope', '$modalInstance', '$state', 'TodoService', 'todoEntry', 'successCallback', 'errorCallback', + function($log, $scope, $modalInstance, $state, TodoService, todoEntry, successCallback, errorCallback) { + var logger = $log.getInstance('app.todo.directives.DeleteTodoController'); + + logger.info('Showing delete confirmation dialog for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + + $scope.cancel = function() { + logger.info('User clicked cancel button. Todo entry is not deleted.'); + $modalInstance.dismiss('cancel'); + }; + + $scope.delete = function() { + logger.info('User clicked delete button. Todo entry is deleted.'); + $modalInstance.close(); + TodoService.delete(todoEntry, successCallback, errorCallback); + }; + }]) + .directive('deleteTodoEntryButton', ['$modal', '$state', 'NotificationService', function($modal, $state, NotificationService) { + return { + link: function (scope, element, attr) { + scope.onSuccess = function() { + NotificationService.flashMessage('todo.notifications.delete.success', 'success'); + $state.go('todo.list'); + }; + + scope.onError = function() { + NotificationService.flashMessage('todo.notifications.delete.error', 'error'); + }; + + scope.showDeleteConfirmationDialog = function() { + $modal.open({ + templateUrl: 'todo/delete-todo-modal.html', + controller: 'DeleteTodoController', + resolve: { + errorCallback: function() { + return scope.onError; + }, + successCallback: function() { + return scope.onSuccess; + }, + todoEntry: function () { + return scope.todoEntry; + } + } + }); + }; + }, + template: '', + scope: { + todoEntry: '=' + } + }; + }]) + .directive('todoEntryForm', ['$log', '$state', 'NotificationService', 'TodoService', function($log, $state, NotificationService, TodoService) { + var logger = $log.getInstance('app.todo.directives.todoEntryForm'); + + return { + link: function (scope, element, attr) { + scope.saveTodoEntry = function() { + logger.info('Saving todo entry: %j', scope.todoEntry); + + var onSuccess = function(saved) { + NotificationService.flashMessage(scope.successMessageKey, 'success'); + $state.go('todo.view', {id: saved.id}); + }; + + var onError = function() { + NotificationService.flashMessage(scope.errorMessageKey, 'errors'); + }; + + if (scope.formType === 'add') { + TodoService.add(scope.todoEntry, onSuccess, onError); + } + else if (scope.formType === 'edit') { + TodoService.update(scope.todoEntry, onSuccess, onError); + } + else { + logger.error('Unknown form type: %s', scope.formType); + } + }; + }, + templateUrl: 'todo/todo-form-directive.html', + scope: { + errorMessageKey: '@', + formType: '@', + todoEntry: '=', + successMessageKey: '@' + } + }; + }]) + .directive('todoEntryList', [function() { + return { + templateUrl: 'todo/todo-list-directive.html', + scope: { + todoEntries: '=' + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/module/todo/todo.services.js b/custom-method-all-repos/frontend/app/module/todo/todo.services.js new file mode 100644 index 0000000..f49f622 --- /dev/null +++ b/custom-method-all-repos/frontend/app/module/todo/todo.services.js @@ -0,0 +1,61 @@ +'use strict'; + +angular.module('app.todo.services', ['ngResource']) + .factory('TodoService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/:id', {"id": "@id"}, { + get: {method: 'GET'}, + save: {method: 'POST'}, + update: {method: 'PUT'}, + query: {method: 'GET', params: {}, isArray: true} + }); + + var logger = $log.getInstance('app.todo.services.TodoService'); + + return { + add: function(todo, successCallback, errorCallback) { + logger.info('Adding new todo entry: %j', todo); + return api.save(todo, + function(added) { + logger.info('Added a new todo entry: %j', added); + successCallback(added); + }, + function(error) { + logger.error('Adding a todo entry failed because of an error: %j', error); + errorCallback(error); + }); + }, + delete: function(todo, successCallback, errorCallback) { + logger.info('Deleting todo entry: %j', todo); + return api.delete(todo, + function(deleted) { + logger.info('Deleted todo entry: %j', deleted); + successCallback(deleted); + }, + function(error) { + logger.error('Deleting the todo entry failed because of an error: %j', error); + errorCallback(error); + } + ); + }, + findAll: function() { + logger.info('Finding all todo entries.'); + return api.query(); + }, + findById: function(id) { + logger.info('Finding todo entry by id: %s', id); + return api.get({id: id}).$promise; + }, + update: function(todo, successCallback, errorCallback) { + logger.info('Updating todo entry: %j', todo); + return api.update(todo, + function(updated) { + logger.info('Updated the information of the todo entry: %j', updated); + successCallback(updated); + }, + function(error) { + logger.error('Updating the information of the todo entry failed because of an error: %j', error); + errorCallback(error); + }); + } + }; + }]); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/app/styles/app.less b/custom-method-all-repos/frontend/app/styles/app.less new file mode 100644 index 0000000..4e70998 --- /dev/null +++ b/custom-method-all-repos/frontend/app/styles/app.less @@ -0,0 +1,74 @@ +[ng-cloak] { + display: none; +} + +@import "https://github.com/bower_components/bootstrap/less/bootstrap.less"; + +// Red asterisk for required labels +label.required:before{ + content:"* "; + color:red; +} + +// styles for custom input validation +input.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +input.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +textarea.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +small.ng-error { + color: #a94442; +} + +a:hover { + cursor: pointer; +} + +.striped-list { + > .row:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + +.striped-list .row { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; +} + +.action-buttons { + text-align: right; +} diff --git a/custom-method-all-repos/frontend/bower.json b/custom-method-all-repos/frontend/bower.json new file mode 100644 index 0000000..c19dab2 --- /dev/null +++ b/custom-method-all-repos/frontend/bower.json @@ -0,0 +1,39 @@ +{ + "name": "Spring Data JPA Tutorial - Query Methods", + "version": "0.0.1", + "main": "_public/frontend/js/app.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "console-polyfill": "~0.2.1", + "lodash": "~3.8.0", + "moment": "2.10.6", + "jquery": "2.1.0", + "bootstrap": "~3.3.4", + "angular": "~1.3.15", + "angular-http-auth": "1.2.2", + "angular-i18n": "~1.3.15", + "angular-moment": "0.10.1", + "angular-logger": "1.0.1", + "angular-sanitize": "~1.3.15", + "angular-resource": "~1.3.15", + "angular-cookies": "~1.3.15", + "angular-loader": "~1.3.15", + "angular-mocks": "~1.3.15", + "angular-translate": "~2.7.0", + "angular-translate-storage-local": "~2.7.0", + "angular-translate-loader-static-files": "~2.7.0", + "angular-translate-handler-log": "~2.7.0", + "angular-ui-utils": "~0.2.3", + "angular-ui-router": "~0.2.15", + "angular-bootstrap": "~0.13.0", + "angular-growl-v2": "0.7.3", + "es5-shim": "~4.1.1", + "json3": "~3.3.2", + "script.js": "~2.5.7", + "sprintf": "1.0.3" + } +} diff --git a/custom-method-all-repos/frontend/build.config.js b/custom-method-all-repos/frontend/build.config.js new file mode 100644 index 0000000..1ec8575 --- /dev/null +++ b/custom-method-all-repos/frontend/build.config.js @@ -0,0 +1,76 @@ +'use strict'; + +var path = require('path'); + +var targetBase = './build/'; + +module.exports = { + //Configures the directories in which the files created by Gulp are copied. + target: { + js: targetBase + '/js', + lib: path.join(targetBase, 'js', 'lib'), + css: path.join(targetBase, 'css'), + partials: path.join(targetBase, 'partials'), + assets: targetBase + }, + + //Configures the location of the used libraries and frameworks. + vendorFiles: { + code: [ + './bower_components/console-polyfill/index.js', + './bower_components/lodash/dist/lodash.min.js', + './bower_components/jquery/dist/jquery.min.js', + './bower_components/angular/angular.js', + './bower_components/moment/min/moment-with-locales.min.js', + './bower_components/sprintf/dist/sprintf.min.js', + './bower_components/angular-http-auth/src/http-auth-interceptor.js', + './bower_components/angular-i18n/angular-locale_fi-fi.js', + './bower_components/angular-cookies/angular-cookies.min.js', + './bower_components/angular-moment/angular-moment.min.js', + './bower_components/angular-logger/dist/angular-logger.min.js', + './bower_components/angular-resource/angular-resource.min.js', + './bower_components/angular-sanitize/angular-sanitize.min.js', + './bower_components/angular-translate/angular-translate.min.js', + './bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', + './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js', + './bower_components/angular-translate-storage-local/angular-translate-storage-local.min.js', + './bower_components/angular-translate-handler-log/angular-translate-handler-log.min.js', + './bower_components/angular-ui-router/release/angular-ui-router.min.js', + './bower_components/angular-ui-utils/ui-utils.min.js', + './bower_components/angular-ui-utils/ui-utils-ieshiv.min.js', + './bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', + './bower_components/angular-growl-v2/build/angular-growl.min.js', + './vendor/spring-security-csrf-token-interceptor/src/spring-security-csrf-token-interceptor.js' + ] + }, + + //Configures the location of our application's files. + appFiles: { + //Configures the location of the Javascript files. + code: [ + "./app/**/*.js" + ], + //Configures the location of the LESS files. + styleBase: "./app/styles/", + style: [ + "./bower_components/angular-growl-v2/build/angular-growl.min.css", + "./app/styles/app.less" + ], + //Configures the location of the view templates. + partials: [ + "./app/assets/partials/**/*.html" + ], + //Configures the location of static assets such as images, fonts, and localization files. + assetsBase: './app/assets/', + assets: [ + './app/assets/**' + ], + //Configures the location of shims (libraries that bring new APIs to older browsers) + shim: [ + './bower_components/angular-loader/angular-loader.min.js', + './bower_components/script.js/dist/script.min.js', + './bower_components/es5-shim/es5-shim.min.js', + './bower_components/json3/lib/json3.min.js' + ] + } +}; diff --git a/custom-method-all-repos/frontend/gulpfile.js b/custom-method-all-repos/frontend/gulpfile.js new file mode 100644 index 0000000..40f10f7 --- /dev/null +++ b/custom-method-all-repos/frontend/gulpfile.js @@ -0,0 +1,124 @@ +var gulp = require("gulp"); +var plugins = require('gulp-load-plugins')(); +var config = require('./build.config.js'); + +//Analyzes the Javascript files of our application by using JSHint and reports the found problems. +gulp.task('jshint', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.changed(config.target.js)) + .pipe(plugins.jshint('.jshintrc')) + .pipe(plugins.jshint.reporter('jshint-stylish')); +}); + +//Processes the Javascript files of our application. +gulp.task('appCode', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.sourcemaps.init()) + //Combines the Javascript files into a single Javascript file + .pipe(plugins.concat('app.min.js')) + //Minifies the created Javascript file + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.sourcemaps.write()) + //Copies the minified Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file. + .pipe(plugins.size({title: 'application'})) +}); + +//Processes the HTML templates of our application. +gulp.task('appPartials', function () { + return gulp.src(config.appFiles.partials) + .pipe(plugins.changed(config.target.js)) + //Minifies the HTML files + .pipe(plugins.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + //Loads the HTML templates into AngularJS $templateCache + .pipe(plugins.angularTemplatecache('partials.js', { + standalone: true + })) + //Copy the created Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of created Javascript file + .pipe(plugins.size({showFiles: true})) +}); + +//Processes the LESS files of our application. +gulp.task('appLess', function () { + return gulp.src(config.appFiles.style) + //Creates the final CSS file + .pipe(plugins.less({ + paths: [config.appFiles.styleBase] + })) + .pipe(plugins.concat('app.css')) + //Minifies the created CSS file + .pipe(plugins.minifyCss()) + //Copies the CSS File into the target directory + .pipe(gulp.dest(config.target.css)) + //Reports the size of the final CSS file. + .pipe(plugins.size({ title: 'css' })) +}); + +gulp.task('appAssets', function () { + return gulp.src(config.appFiles.assets, {base: config.appFiles.assetsBase}) + .pipe(gulp.dest(config.target.assets)) +}); + +//Minimizes the shims used by our application and copies them to the target directory. +gulp.task('appShim', function () { + return gulp.src(config.appFiles.shim) + .pipe(plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + })) + .pipe(gulp.dest(config.target.lib)); +}); + +//Processes the Javascript files of the libraries and frameworks that are used in our application +gulp.task('vendorCode', function () { + return gulp.src(config.vendorFiles.code) + //Combine the Javascript files into a single Javascript file + .pipe(plugins.concat('vendor.min.js')) + //Skips minification of files that are already minified. + .pipe(plugins.if('*.min.js', plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + }))) + //Minifies Javascript files that are not minified. + .pipe(plugins.if('vendor/**/*.js', plugins.uglify({ + mangle: false, + compress: true + }))) + //Copies the created file to the target directory. + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file + .pipe(plugins.size({title: 'vendor'})) +}); + +//Analyzes our Javascript files by using JSHint and invokes the build when the watched files are changed +gulp.task('watch', ['jshint', 'build'], function () { + gulp.watch(config.appFiles.partials, ['appPartials']); + gulp.watch(config.appFiles.code, ['appCode', 'jshint']); + gulp.watch(config.appFiles.style, ['appLess']); + gulp.watch(config.appFiles.assets, ['appAssets']); + gulp.watch(config.vendorFiles.code, ['vendorCode']); +}); + +//Configures the tasks of our build +gulp.task('build', [ + 'appLess', + 'appShim', + 'appAssets', + 'appPartials', + 'appCode', + 'vendorCode' +]); + +//Runs the watch task if no task is specified when gulp is run +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/custom-method-all-repos/frontend/package.json b/custom-method-all-repos/frontend/package.json new file mode 100644 index 0000000..55dfccd --- /dev/null +++ b/custom-method-all-repos/frontend/package.json @@ -0,0 +1,38 @@ +{ + "author": "Petri Kainulainen", + "name": "spring-data-jpa-tutorial-query-methods", + "description": "Angular frontend for a Spring Data JPA example.", + "version": "1.0.0", + "homepage": "", + "repository": { + "type": "git", + "url": "" + }, + "dependencies": { + "bower": "~1.4.1", + "gulp": "~3.8.11", + "gulp-angular-templatecache": "~1.6.0", + "gulp-changed": "~1.2.1", + "gulp-concat": "~2.5.2", + "gulp-if": "~1.2.5", + "gulp-insert": "^0.4.0", + "gulp-jshint": "~1.10.0", + "gulp-less": "~3.0.3", + "gulp-load-plugins": "~0.10.0", + "gulp-minify-css": "~1.1.1", + "gulp-minify-html": "~1.0.2", + "gulp-rename": "~1.2.2", + "gulp-size": "~1.2.1", + "gulp-sourcemaps": "~1.5.2", + "gulp-uglify": "~1.2.0", + "jshint-stylish": "~1.0.2" + }, + "engines": { + "node": ">=0.12.0" + } +} + + + + + diff --git a/custom-method-all-repos/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js b/custom-method-all-repos/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js new file mode 100644 index 0000000..84318da --- /dev/null +++ b/custom-method-all-repos/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("spring-security-csrf-token-interceptor",[]).factory("csrfInterceptor",["$injector","$q",function($injector){var $q=$injector.get("$q"),csrf=$injector.get("csrf"),csrfService=csrf.init();return{request:function(config){return csrfService.settings.httpTypes.indexOf(config.method.toUpperCase())>-1&&(config.headers[csrfService.settings.csrfTokenHeader]=csrfService.token),config||$q.when(config)},responseError:function(response){var $http,newToken=response.headers(csrfService.settings.csrfTokenHeader);return 403===response.status&&csrfService.numRetries -1) { + config.headers[csrfService.settings.csrfTokenHeader] = csrfService.token; + } + return config || $q.when(config); + }, + responseError: function(response) { + var $http, + newToken = response.headers(csrfService.settings.csrfTokenHeader); + + if (response.status === 403 && csrfService.numRetries < csrfService.settings.maxRetries) { + csrfService.getTokenData(); + $http = $injector.get('$http'); + csrfService.numRetries = csrfService.numRetries + 1; + return $http(response.config); + } else if (newToken) { + // update the csrf token in-case of response errors other than 403 + csrfService.token = newToken; + } + // Fix for interceptor causing failing requests + return $q.reject(response); + }, + response: function(response) { + // reset number of retries on a successful response + csrfService.numRetries = 0; + return response; + } + }; + } + ]).factory('csrfService', [ + + function() { + var defaults = { + url: '/', // the URL to which the CSRF call has to be made to get the token + csrfHttpType: 'head', // the HTTP method type which is used for making the CSRF token call + maxRetries: 5, // number of retires allowed for forbidden requests + csrfTokenHeader: 'X-CSRF-TOKEN', + httpTypes: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] // default allowed HTTP types + }; + return { + inited: false, + settings: null, + numRetries: 0, + token: '', + init: function(options) { + this.settings = angular.extend({}, defaults, options); + this.getTokenData(); + console.log(this.settings, this.defaults, options); + }, + getTokenData: function() { + var xhr = new XMLHttpRequest(); + xhr.open(this.settings.csrfHttpType, this.settings.url, false); + xhr.send(); + + this.token = xhr.getResponseHeader(this.settings.csrfTokenHeader); + this.inited = true; + } + }; + + } + ]).provider('csrf', [ + + function() { + var CsrfModel = function CsrfModel(options) { + return { + options: options, + csrfService: null + }; + }; + + return { + $get: ['csrfService', + function(csrfService) { + var self = this; + return { + init: function() { + self.model = new CsrfModel(self.options); + self.model.csrfService = csrfService; + self.model.csrfService.init(self.model.options); + return self.model.csrfService; + } + }; + } + ], + + model: null, + + options: {}, + + config: function(options) { + this.options = options; + } + }; + } + ]).config(['$httpProvider', + function($httpProvider) { + $httpProvider.interceptors.push('csrfInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/custom-method-all-repos/pom.xml b/custom-method-all-repos/pom.xml new file mode 100644 index 0000000..f0d1c49 --- /dev/null +++ b/custom-method-all-repos/pom.xml @@ -0,0 +1,375 @@ + + 4.0.0 + net.petrikainulainen.springdata.jpa + custom-methods-all-repos + 0.1 + Spring Data JPA - Adding Custom Methods Into All Repositories + war + + This example demonstrates how you can add custom methods into all + Spring Data JPA repositories. + + + + + + io.spring.platform + platform-bom + 1.1.2.RELEASE + pom + import + + + + + + 1.8 + UTF-8 + true + false + 4.0.1.RELEASE + + + + + dev + + + integration-test + + false + true + + + + + + + + org.apache.commons + commons-lang3 + + + + org.slf4j + slf4j-api + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + + com.h2database + h2 + + + + com.zaxxer + HikariCP + + + + org.hibernate + hibernate-entitymanager + + + + org.jadira.usertype + usertype.extended + 3.2.0.GA + + + + org.springframework.data + spring-data-jpa + + + + org.springframework + spring-aspects + + + org.springframework + spring-context-support + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet + jstl + + + org.springframework + spring-webmvc + + + org.hibernate + hibernate-validator + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + + + javax.el + javax.el-api + test + + + org.glassfish.web + el-impl + 2.2 + test + + + junit + junit + test + + + com.nitorcreations + junit-runners + 1.3 + test + + + org.assertj + assertj-core + 3.1.0 + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + info.solidsoft.mockito + mockito-java8 + 0.3.0 + test + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.security.version} + test + + + com.jayway.jsonpath + json-path + test + + + com.jayway.jsonpath + json-path-assert + 0.9.1 + test + + + com.github.springtestdbunit + spring-test-dbunit + 1.2.1 + test + + + org.dbunit + dbunit + 2.5.1 + test + + + junit + junit + + + + + + ROOT + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-integration-test-sources + generate-test-sources + + add-test-source + + + + src/integration-test/java + + + + + add-integration-test-resources + generate-test-resources + + add-test-resource + + + + + src/integration-test/resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${jdk.version} + ${jdk.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + ROOT + false + + + frontend/build + / + false + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + ${skip.unit.tests} + + **/IT*.java + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18 + + + + integration-tests + + integration-test + verify + + + + ${skip.integration.tests} + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.2.10.v20150310 + + 0 + stop + 9999 + + + spring.profiles.active + application + + + + ${project.basedir}/target/ROOT.war + / + + ${project.basedir}/src/main/webapp + ${project.basedir}/frontend/build + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + + run + + + + + + + diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java new file mode 100644 index 0000000..e018718 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * This class contains the constants that are used in our integration tests, DbUnit datasets, + * and the localization file. + * + * @author Petri Kainulainen + */ +public final class TodoConstants { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T13:13:28+02:00"; + public static final String DESCRIPTION = "description"; + public static final Long ID = 1L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T13:13:28+02:00"; + public static final String TITLE = "title"; + + public static final String SEARCH_TERM_DESCRIPTION_MATCHES = "esC"; + public static final String SEARCH_TERM_NO_MATCH = "NO MATCH"; + public static final String SEARCH_TERM_TITLE_MATCHES = "It"; + + public static final String UPDATED_DESCRIPTION = "updatedDescription"; + public static final String UPDATED_TITLE = "updatedTitle"; + + public static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; + public static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; + public static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; + public static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; + + /** + * Prevents instantiation + */ + private TodoConstants() {} +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java new file mode 100644 index 0000000..77cdb31 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * @author Petri Kainulainen + */ +public enum Users { + + USER("user", "password", "ROLE_USER"); + + private String password; + private String role; + private String username; + + Users(String username, String password, String role) { + this.password = password; + this.role = role; + this.username = username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } + + public String getUsername() { + return username; + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java new file mode 100644 index 0000000..425353e --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java @@ -0,0 +1,107 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITTodoRepositoryTest { + + private static final Long NOT_FOUND_ID = 9999L; + private static final String SEARCH_TERM = "tIo"; + private static final Long SECOND_TODO_ID = 2L; + + @Autowired + private TodoRepository repository; + + @Test + @ExpectedDatabase("todo-entries.xml") + public void deleteById_TodoEntryNotFound_ShouldNotMakeAnyChangesToDatabase() { + repository.deleteById(NOT_FOUND_ID); + } + + @Test + public void deleteById_TodoEntryNotFound_ShouldReturnEmptyOptional() { + Optional deleted = repository.deleteById(NOT_FOUND_ID); + assertThat(deleted).isEmpty(); + } + + @Test + @ExpectedDatabase("delete-todo-entry-expected.xml") + public void deleteById_TodoEntryFound_ShouldDeleteTodoEntryFromDatabase() { + repository.deleteById(TodoConstants.ID); + } + + @Test + public void deleteById_TodoEntryFound_ShouldReturnDeletedTodoEntry() { + Optional deleted = repository.deleteById(TodoConstants.ID); + assertThat(deleted.get().getId()).isEqualTo(TodoConstants.ID); + } + + @Test + public void findBySearchTerm_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.ID); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTerm_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.ID); + } + + @Test + public void findBySearchTerm_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTerm(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(SECOND_TODO_ID); + + Todo second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.ID); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java new file mode 100644 index 0000000..af912d1 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java @@ -0,0 +1,27 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.dataset.FlatXmlDataSetLoader; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ReplacementDataSet; +import org.springframework.core.io.Resource; +/** + * This class is a custom DbUnit data set loader that support flat XML data sets. This data set loader + * adds support for the extra features: + *
    + *
  • You can use the column sensing feature of DbUnit.
  • + *
  • You can specify that a column's value is null by using the string [null].
  • + *
+ * @author Petri Kainulainen + */ +public class ColumnSensingReplacementDataSetLoader extends FlatXmlDataSetLoader { + + @Override + protected IDataSet createDataSet(Resource resource) throws Exception { + return createReplacementDataSet(super.createDataSet(resource)); + } + private ReplacementDataSet createReplacementDataSet(IDataSet dataSet) { + ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet); + replacementDataSet.addReplacementObject("[null]", null); + return replacementDataSet; + } +} \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java new file mode 100644 index 0000000..4360756 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Petri Kainulainen + */ +public final class DbTestUtil { + + private DbTestUtil() {} + + public static void resetAutoIncrementColumns(ApplicationContext applicationContext, + String... tableNames) throws SQLException { + DataSource dataSource = applicationContext.getBean(DataSource.class); + String resetSqlTemplate = getResetSqlTemplate(applicationContext); + try (Connection dbConnection = dataSource.getConnection()) { + //Create SQL statements that reset the auto increment columns and invoke + //the created SQL statements. + for (String resetSqlArgument: tableNames) { + try (Statement statement = dbConnection.createStatement()) { + String resetSql = String.format(resetSqlTemplate, resetSqlArgument); + statement.execute(resetSql); + } + } + } + } + + private static String getResetSqlTemplate(ApplicationContext applicationContext) { + //Read the SQL template from the properties file + Environment environment = applicationContext.getBean(Environment.class); + return environment.getRequiredProperty("test.reset.sql.template"); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java new file mode 100644 index 0000000..d3f6fe1 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java @@ -0,0 +1,251 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +@DatabaseSetup("no-todo-entries.xml") +public class ITCreateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + DbTestUtil.resetAutoIncrementColumns(webAppContext, "todos"); + + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void create_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isCreated()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.creationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } + + @Test + @ExpectedDatabase(value = "create-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java new file mode 100644 index 0000000..341b271 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITDeleteTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void delete_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("delete-todo-entry-expected.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java new file mode 100644 index 0000000..c1d648c --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java @@ -0,0 +1,97 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindAllTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findAll_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findAll_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenTodoEntriesAreNotFound_ShouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenOneTodoEntryIsFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java new file mode 100644 index 0000000..eac4311 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java @@ -0,0 +1,107 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindByIdTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void findById_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java new file mode 100644 index 0000000..e8cac00 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java @@ -0,0 +1,216 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("two-todo-entries.xml") +public class ITFindBySearchTermTest { + + private static final String SECOND_TODO_CREATED_BY_USER = "createdByUser"; + private static final String SECOND_TODO_CREATION_TIME = "2014-12-24T13:13:28+02:00"; + private static final String SECOND_TODO_DESCRIPTION = "tiscription"; + private static final Long SECOND_TODO_ID = 2L; + private static final String SECOND_TODO_MODIFIED_BY_USER = "modifiedByUser"; + private static final String SECOND_TODO_MODIFICATION_TIME = "2014-12-25T13:13:28+02:00"; + private static final String SECOND_TODO_TITLE = "First"; + + private static final String SEARCH_TERM = "tIo"; + + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findBySearchTerm_AsAnonymous_ShouldReturnHttpResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnZeroTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTerm_ShouldTwoTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].createdByUser", is(SECOND_TODO_CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(SECOND_TODO_CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(SECOND_TODO_DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(SECOND_TODO_ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(SECOND_TODO_MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(SECOND_TODO_MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(SECOND_TODO_TITLE))) + .andExpect(jsonPath("$[1].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[1].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[1].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[1].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[1].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[1].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[1].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenSearchTermIsEmpty_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, "") + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenSearchTermIsEmpty_ShouldTwoTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, "") + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].createdByUser", is(SECOND_TODO_CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(SECOND_TODO_CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(SECOND_TODO_DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(SECOND_TODO_ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(SECOND_TODO_MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(SECOND_TODO_MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(SECOND_TODO_TITLE))) + .andExpect(jsonPath("$[1].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[1].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[1].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[1].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[1].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[1].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[1].title", is(TodoConstants.TITLE))); + } +} \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java new file mode 100644 index 0000000..bd7c57e --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java @@ -0,0 +1,327 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITUpdateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void update_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.UPDATED_DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.UPDATED_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase(value = "update-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java new file mode 100644 index 0000000..e410ded --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java @@ -0,0 +1,79 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import net.petrikainulainen.springdata.jpa.web.WebTestConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITGetAuthenticatedUserTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void getAuthenticatedUser_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnUserInformationAsJSON() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.username", is("user"))) + .andExpect(jsonPath("$.role", is(UserRole.ROLE_USER.name()))); + } +} diff --git a/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java new file mode 100644 index 0000000..a7d93ff --- /dev/null +++ b/custom-method-all-repos/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITLoginTest { + + private static final String INVALID_PASSWORD = "invalidPassword"; + private static final String INVALID_USERNAME = "invalidUsername"; + + private static final String PARAM_NAME_PASSWORD = "password"; + private static final String PARAM_NAME_USERNAME = "username"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void logIn_WhenUsernameIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, INVALID_USERNAME) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenPasswordIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, INVALID_PASSWORD) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldReturnResponseStatusFound() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isFound()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldRedirectClientToControllerMethodThatReturnsAuthenticatedUser() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(redirectedUrl("/api/authenticated-user")); + } +} diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/delete-todo-entry-expected.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/delete-todo-entry-expected.xml new file mode 100644 index 0000000..51ea80f --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/delete-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml new file mode 100644 index 0000000..45bf713 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml new file mode 100644 index 0000000..12e0c00 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml new file mode 100644 index 0000000..50193f2 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml new file mode 100644 index 0000000..0c1e6bc --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml new file mode 100644 index 0000000..fbb3e27 --- /dev/null +++ b/custom-method-all-repos/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/ant/build.xml b/custom-method-all-repos/src/main/ant/build.xml new file mode 100644 index 0000000..90d4c18 --- /dev/null +++ b/custom-method-all-repos/src/main/ant/build.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java new file mode 100644 index 0000000..6a9566b --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * This class obtains the current time by using a {@link DateTimeService} + * object. The reason for this is that we can use a different implementation in our integration tests. + * + * In other words: + *
    + *
  • + * Our application always returns the correct time because it uses the + * {@link CurrentTimeDateTimeService} class. + *
  • + *
  • + * When our integration tests are running, we can return a constant time which gives us the possibility + * to assert the creation and modification times saved to the database. + *
  • + *
+ * + * @author Petri Kainulainen + */ +public class AuditingDateTimeProvider implements DateTimeProvider { + + private final DateTimeService dateTimeService; + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Calendar getNow() { + return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepository.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepository.java new file mode 100644 index 0000000..31f8316 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepository.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; + +import java.io.Serializable; +import java.util.Optional; + +/** + * This interface is the base interface that must be extended by all Spring Data JPA + * repositories of our example application. It also declares the custom methods that + * are added into every Spring Data JPA repository. + * + * @author Petri Kainulainen + */ +@NoRepositoryBean +public interface BaseRepository extends Repository { + + /** + * Deletes a managed entity. + * @param id The id of the deleted entity. + * @return an {@code Optional} that contains the deleted entity. If there + * is no entity that has the given id, this method returns an empty {@code Optional}. + */ + Optional deleteById(ID id); +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryFactoryBean.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryFactoryBean.java new file mode 100644 index 0000000..2b296f0 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryFactoryBean.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import javax.persistence.EntityManager; +import java.io.Serializable; + +/** + * This factory bean replaces the default implementation of our repository interfaces + * with the {@link BaseRepositoryImpl}. This ensures that the {@link BaseRepository#deleteById(Serializable)} + * method is added into all Spring Data JPA repositories of our example application. + * + * @author Petri Kainulainen + */ +public class BaseRepositoryFactoryBean, T, + I extends Serializable> extends JpaRepositoryFactoryBean { + + @Override + protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) { + return new BaseRepositoryFactory(em); + } + + private static class BaseRepositoryFactory + extends JpaRepositoryFactory { + + private final EntityManager em; + + public BaseRepositoryFactory(EntityManager em) { + super(em); + this.em = em; + } + + @Override + protected Object getTargetRepository(RepositoryMetadata metadata) { + return new BaseRepositoryImpl((Class) metadata.getDomainType(), em); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return BaseRepositoryImpl.class; + } + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryImpl.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryImpl.java new file mode 100644 index 0000000..76131e8 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepositoryImpl.java @@ -0,0 +1,49 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.io.Serializable; +import java.util.Optional; + +/** + * This class is our "custom" implementation of the {@link org.springframework.data.repository.CrudRepository} + * interface that replaces the default implementation provided by Spring Data JPA. + * + * @author Petri Kainulainen + */ +public class BaseRepositoryImpl + extends SimpleJpaRepository implements BaseRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseRepositoryImpl.class); + + private final EntityManager entityManager; + + public BaseRepositoryImpl(Class domainClass, EntityManager entityManager) { + super(domainClass, entityManager); + this.entityManager = entityManager; + } + + @Transactional + @Override + public Optional deleteById(ID id) { + LOGGER.info("Deleting an entity by using id: {}", id); + + T deleted = entityManager.find(this.getDomainClass(), id); + LOGGER.debug("Deleted entity is: {}", deleted); + + Optional returned = Optional.empty(); + + if (deleted != null) { + entityManager.remove(deleted); + returned = Optional.of(deleted); + } + + LOGGER.info("Returning deleted entity: {}", returned); + + return returned; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java new file mode 100644 index 0000000..012085c --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * This class is used in our integration tests and it always returns the + * same time. This gives us the possibility to verify that the correct + * timestamps are saved to the database. + * + * @author Petri Kainulainen + */ +public class ConstantDateTimeService implements DateTimeService { + + public static final String CURRENT_DATE_AND_TIME = getConstantDateAndTime(); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConstantDateTimeService.class); + + private static String getConstantDateAndTime() { + return "2015-07-19T12:52:28" + + getSystemZoneOffset() + + getSystemZoneId(); + } + + private static String getSystemZoneOffset() { + return "+03:00"; + } + + private static String getSystemZoneId() { + return "[" + ZoneId.systemDefault().toString() + "]"; + } + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime constantDateAndTime = ZonedDateTime.from(FORMATTER.parse(CURRENT_DATE_AND_TIME)); + + LOGGER.info("Returning constant date and time: {}", constantDateAndTime); + + return constantDateAndTime; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java new file mode 100644 index 0000000..2812fb0 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +/** + * This class returns the current time. + * + * @author Petri Kainulainen + */ +public class CurrentTimeDateTimeService implements DateTimeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CurrentTimeDateTimeService.class); + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime currentDateAndTime = ZonedDateTime.now(); + + LOGGER.info("Returning current date and time: {}", currentDateAndTime); + + return currentDateAndTime; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java new file mode 100644 index 0000000..a1e1a11 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java @@ -0,0 +1,18 @@ +package net.petrikainulainen.springdata.jpa.common; + +import java.time.ZonedDateTime; + +/** + * This interface defines the methods used to get the current + * date and time. + * + * @author Petri Kainulainen + */ +public interface DateTimeService { + + /** + * Returns the current date and time. + * @return + */ + ZonedDateTime getCurrentDateAndTime(); +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java new file mode 100644 index 0000000..46f2849 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * This controller is responsible of starting the frontend application. + * @author Petri Kainulainen + */ +@Controller +public class FrontendLoaderController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FrontendLoaderController.class); + + private static final String FRONTEND_APPLICATION_VIEW = "frontend/client"; + + /** + * Starts the AngularJS application. + * @return + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public String startAngularJSApplication() { + LOGGER.debug("Starting frontend single page application."); + return FRONTEND_APPLICATION_VIEW; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java new file mode 100644 index 0000000..d3cf557 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.common; + +/** + * This class provides static utility methods that are used to ensure that a constructor or a method was invoked properly. + * These methods throw an exception if the specified precondition is violated. + * + * This class selects the thrown exception by using the guideline given in Effective Java by Joshua Bloch (Item 60). + * + * @author Petri Kainulainen + */ +public final class PreCondition { + + private PreCondition() {} + + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessageTemplate The template that is used to construct the message of the exception thrown if the + * inspected exception is false. The template must use the syntax that is supported + * by the {@link java.lang.String#format(String, Object...)} method. + * @param errorMessageArguments The arguments that are used when the message of the thrown exception is constructed. + * @throws java.lang.IllegalArgumentException if the inspected exception is false. + */ + public static void isTrue(boolean expression, String errorMessageTemplate, Object... errorMessageArguments) { + isTrue(expression, String.format(errorMessageTemplate, errorMessageArguments)); + } + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessage The error message that is passed forward to the exception that is thrown + * if the expression is false. + * @throws java.lang.IllegalArgumentException if the inspected expression is false. + */ + public static void isTrue(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the string given as a method parameter is not empty. + * @param string The inspected string. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the string is empty. + * @throws java.lang.IllegalArgumentException if the inspected string is empty. + */ + public static void notEmpty(String string, String errorMessage) { + if (string.isEmpty()) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the object given as a method parameter is not null. + * @param reference A reference to the inspected object. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the object given as a method parameter is null. + * @throws java.lang.NullPointerException If the object given as a method parameter is null. + */ + public static void notNull(Object reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java new file mode 100644 index 0000000..ed511d8 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java @@ -0,0 +1,34 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/** + * This component returns the username of the authenticated user. + * + * @author Petri Kainulainen + */ +public class UsernameAuditorAware implements AuditorAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(UsernameAuditorAware.class); + + @Override + public String getCurrentAuditor() { + LOGGER.debug("Getting the username of authenticated user."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + LOGGER.debug("Current user is anonymous. Returning null."); + return null; + } + + String username = ((User) authentication.getPrincipal()).getUsername(); + LOGGER.debug("Returning username: {}", username); + + return username; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java new file mode 100644 index 0000000..0f922d8 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.common.CurrentTimeDateTimeService; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * @author Petri Kainulainen + */ +@Configuration +@ComponentScan("net.petrikainulainen.springdata.jpa") +@Import({WebMvcContext.class, PersistenceContext.class, SecurityContext.class}) +public class ExampleApplicationContext { + + private static final String MESSAGE_SOURCE_BASE_NAME = "i18n/messages"; + + /** + * These static classes are required because it makes it possible to use + * different properties files for every Spring profile. + * + * See: This StackOverflow answer for more details. + */ + @Profile(Profiles.APPLICATION) + @Configuration + @PropertySource("classpath:application.properties") + static class ApplicationProperties {} + + @Profile(Profiles.APPLICATION) + @Bean + DateTimeService currentTimeDateTimeService() { + return new CurrentTimeDateTimeService(); + } + + @Profile(Profiles.INTEGRATION_TEST) + @Configuration + @PropertySource("classpath:integration-test.properties") + static class IntegrationTestProperties {} + + @Profile(Profiles.INTEGRATION_TEST) + @Bean + DateTimeService constantDateTimeService() { + return new ConstantDateTimeService(); + } + + @Bean + MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(MESSAGE_SOURCE_BASE_NAME); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } + + @Bean + PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java new file mode 100644 index 0000000..7648dc8 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java @@ -0,0 +1,146 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.petrikainulainen.springdata.jpa.common.AuditingDateTimeProvider; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import net.petrikainulainen.springdata.jpa.common.UsernameAuditorAware; +import net.petrikainulainen.springdata.jpa.common.BaseRepositoryFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +/** + * This configuration class configures the persistence layer of our example application and + * enables annotation driven transaction management. + * + * This configuration is put to a single class because this way we can write integration + * tests for our persistence layer by using the configuration used by our example + * application. In other words, we can ensure that the persistence layer of our application + * works as expected. + * + * @author Petri Kainulainen + */ +@Configuration +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") +@EnableJpaRepositories(basePackages = {"net.petrikainulainen.springdata.jpa.todo"}, + repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class +) +@EnableTransactionManagement +class PersistenceContext { + private static final String[] ENTITY_PACKAGES = { + "net.petrikainulainen.springdata.jpa.todo" + }; + + private static final String PROPERTY_NAME_DB_DRIVER_CLASS = "db.driver"; + private static final String PROPERTY_NAME_DB_PASSWORD = "db.password"; + private static final String PROPERTY_NAME_DB_URL = "db.url"; + private static final String PROPERTY_NAME_DB_USER = "db.username"; + private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; + private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; + private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; + private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; + private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; + + @Bean + AuditorAware auditorProvider() { + return new UsernameAuditorAware(); + } + + @Bean + DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { + return new AuditingDateTimeProvider(dateTimeService); + } + + /** + * Creates and configures the HikariCP datasource bean. + * @param env The runtime environment of our application. + * @return + */ + @Bean(destroyMethod = "close") + DataSource dataSource(Environment env) { + HikariConfig dataSourceConfig = new HikariConfig(); + dataSourceConfig.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DB_DRIVER_CLASS)); + dataSourceConfig.setJdbcUrl(env.getRequiredProperty(PROPERTY_NAME_DB_URL)); + dataSourceConfig.setUsername(env.getRequiredProperty(PROPERTY_NAME_DB_USER)); + dataSourceConfig.setPassword(env.getRequiredProperty(PROPERTY_NAME_DB_PASSWORD)); + + return new HikariDataSource(dataSourceConfig); + } + + /** + * Creates the bean that creates the JPA entity manager factory. + * @param dataSource The datasource that provides the database connections. + * @param env The runtime environment of our application. + * @return + */ + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + entityManagerFactoryBean.setPackagesToScan(ENTITY_PACKAGES); + + Properties jpaProperties = new Properties(); + + //Configures the used database dialect. This allows Hibernate to create SQL + //that is optimized for the used database. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); + + //Specifies the action that is invoked to the database when the Hibernate + //SessionFactory is created or closed. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); + + //Configures the naming strategy that is used when Hibernate creates + //new database objects and schema elements + jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); + + //If the value of this property is true, Hibernate writes all SQL + //statements to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); + + //If the value of this property is true, Hibernate will use prettyprint + //when it writes SQL to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); + + entityManagerFactoryBean.setJpaProperties(jpaProperties); + + return entityManagerFactoryBean; + } + + /** + * Creates the jdbc template bean that we use to invoke SQL queries via JDBC. + * @param dataSource The datasource that provides the database connection. + * @return + */ + @Bean + NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + /** + * Creates the transaction manager bean that integrates the used JPA provider with the + * Spring transaction mechanism. + * @param entityManagerFactory The used JPA entity manager factory. + * @return + */ + @Bean + JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java new file mode 100644 index 0000000..bda9711 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.config; + +/** + * This class defines the Spring profiles used in the project. The idea behind this class + * is that it helps us to avoid typos when we are using these profiles. At the moment + * there are two profiles which are described in the following: + *
    + *
  • The APPLICATION profile is used when we run our example application.
  • + *
  • The INTEGRATION_TEST profile is used when we run the integration tests of our example application.
  • + *
+ * + * @author Petri Kainulainen + */ +public class Profiles { + public static final String APPLICATION = "application"; + public static final String INTEGRATION_TEST = "integrationtest"; + /** + * Prevent instantiation. + */ + private Profiles() {} +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java new file mode 100644 index 0000000..8aa95e4 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.web.security.CsrfHeaderFilter; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationEntryPoint; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationFailureHandler; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationSuccessHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CsrfFilter; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebSecurity +class SecurityContext extends WebSecurityConfigurerAdapter { + + @Bean + AuthenticationEntryPoint authenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + AuthenticationFailureHandler authenticationFailureHandler() { + return new RestAuthenticationFailureHandler(); + } + + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler() { + return new RestAuthenticationSuccessHandler(); + } + + @Bean + protected UserDetailsService userDetailsService() { + return super.userDetailsService(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + //Use the custom authentication entry point. + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint()) + .and() + //Configure form login. + .formLogin() + .loginProcessingUrl("/api/login") + .failureHandler(authenticationFailureHandler()) + .successHandler(authenticationSuccessHandler()) + .permitAll() + .and() + //Configure logout function. + .logout() + .deleteCookies("JSESSIONID") + .logoutUrl("/api/logout") + .logoutSuccessUrl("/") + .and() + //Configure url based authorization + .authorizeRequests() + .antMatchers( + "/", + "/api/csrf" + ).permitAll() + .anyRequest().hasRole("USER") + .and() + .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + //Spring Security ignores request to static resources such as CSS or JS files. + .ignoring() + .antMatchers( + "/favicon.ico", + "/css/**", + "/i18n/**", + "/js/**" + ); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java new file mode 100644 index 0000000..f1861a6 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import java.util.EnumSet; + +/** + * @author Petri Kainulainen + */ +public class WebAppConfig implements WebApplicationInitializer { + private static final String CHARACTER_ENCODING_FILTER_ENCODING = "UTF-8"; + private static final String CHARACTER_ENCODING_FILTER_NAME = "characterEncoding"; + private static final String CHARACTER_ENCODING_FILTER_URL_PATTERN = "/*"; + + private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; + private static final String DISPATCHER_SERVLET_MAPPING = "/"; + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(ExampleApplicationContext.class); + + //XmlWebApplicationContext rootContext = new XmlWebApplicationContext(); + //rootContext.setConfigLocation("classpath:applicationContext.xml"); + + configureDispatcherServlet(servletContext, rootContext); + EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + + configureCharacterEncodingFilter(servletContext, dispatcherTypes); + configureSpringSecurityFilter(servletContext, dispatcherTypes); + servletContext.addListener(new ContextLoaderListener(rootContext)); + } + + private void configureDispatcherServlet(ServletContext servletContext, WebApplicationContext rootContext) { + ServletRegistration.Dynamic dispatcher = servletContext.addServlet( + DISPATCHER_SERVLET_NAME, + new DispatcherServlet(rootContext) + ); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); + } + + private void configureCharacterEncodingFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + characterEncodingFilter.setEncoding(CHARACTER_ENCODING_FILTER_ENCODING); + characterEncodingFilter.setForceEncoding(true); + FilterRegistration.Dynamic characterEncoding = servletContext.addFilter(CHARACTER_ENCODING_FILTER_NAME, characterEncodingFilter); + characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, CHARACTER_ENCODING_FILTER_URL_PATTERN); + } + + private void configureSpringSecurityFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); + security.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java new file mode 100644 index 0000000..c016860 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebMvc +class WebMvcContext extends WebMvcConfigurerAdapter { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + + @Override + public void configureMessageConverters(List> converters) { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + + converters.add(converter); + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp("/WEB-INF/jsp/", ".jsp"); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java new file mode 100644 index 0000000..8e74147 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoSearchService implements TodoSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoSearchService.class); + + private final TodoRepository repository; + + @Autowired + public RepositoryTodoSearchService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + @Override + public List findBySearchTerm(String searchTerm) { + LOGGER.info("Finding todo entries by search term: {}", searchTerm); + + List searchResults = repository.findBySearchTerm(searchTerm); + LOGGER.info("Found {} todo entries", searchResults.size()); + + return TodoMapper.mapEntitiesIntoDTOs(searchResults); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java new file mode 100644 index 0000000..777b427 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoService implements TodoCrudService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoService.class); + + private final TodoRepository repository; + + @Autowired + RepositoryTodoService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional + @Override + public TodoDTO create(TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + Todo created = Todo.getBuilder() + .description(newTodoEntry.getDescription()) + .title(newTodoEntry.getTitle()) + .build(); + + created = repository.save(created); + LOGGER.info("Created a new todo entry: {}", created); + + return TodoMapper.mapEntityIntoDTO(created); + } + + @Transactional + @Override + public TodoDTO delete(Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + Optional deleted = repository.deleteById(id); + LOGGER.info("Deleted todo entry: {}", deleted); + + Todo returned = deleted.orElseThrow(() -> new TodoNotFoundException(id)); + return TodoMapper.mapEntityIntoDTO(returned); + } + + @Transactional(readOnly = true) + @Override + public List findAll() { + LOGGER.info("Finding all todo entries."); + + List todoEntries = repository.findAll(); + + LOGGER.info("Found {} todo entries", todoEntries.size()); + + return TodoMapper.mapEntitiesIntoDTOs(todoEntries); + } + + @Transactional(readOnly = true) + @Override + public TodoDTO findById(Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + Todo todoEntry = findTodoEntryById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return TodoMapper.mapEntityIntoDTO(todoEntry); + } + + @Transactional + @Override + public TodoDTO update(TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + Todo updated = findTodoEntryById(updatedTodoEntry.getId()); + updated.update(updatedTodoEntry.getTitle(), updatedTodoEntry.getDescription()); + + //We need to flush the changes or otherwise the returned object + //doesn't contain the updated audit information. + repository.flush(); + + LOGGER.info("Updated the information of the todo entry: {}", updated); + + return TodoMapper.mapEntityIntoDTO(updated); + } + + private Todo findTodoEntryById(Long id) { + Optional todoResult = repository.findOne(id); + return todoResult.orElseThrow(() -> new TodoNotFoundException(id)); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java new file mode 100644 index 0000000..b3acfbb --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java @@ -0,0 +1,196 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.annotations.Type; +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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Version; +import java.time.ZonedDateTime; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.isTrue; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This entity class contains the information of a single todo entry + * and the methods that are used to create new todo entries and to modify + * the information of an existing todo entry. + * + * @author Petri Kainulainen + */ +@Entity +@EntityListeners(AuditingEntityListener.class) +@NamedNativeQuery(name = "Todo.findBySearchTermNamedNative", + query="SELECT * FROM todos t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC", + resultClass = Todo.class +) +@NamedQuery(name = "Todo.findBySearchTermNamed", + query = "SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) " + + "ORDER BY t.title ASC" +) +@Table(name = "todos") +final class Todo { + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(name = "created_by_user", nullable = false) + @CreatedBy + private String createdByUser; + + @Column(name = "creation_time", nullable = false) + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @CreatedDate + private ZonedDateTime creationTime; + + @Column(name = "description", length = MAX_LENGTH_DESCRIPTION) + private String description; + + @Column(name = "modified_by_user", nullable = false) + @LastModifiedBy + private String modifiedByUser; + + @Column(name = "modification_time") + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @LastModifiedDate + private ZonedDateTime modificationTime; + + @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE) + private String title; + + @Version + private long version; + + /** + * Required by Hibernate. + */ + private Todo() {} + + private Todo(Builder builder) { + this.title = builder.title; + this.description = builder.description; + } + + static Builder getBuilder() { + return new Builder(); + } + + Long getId() { + return id; + } + + String getCreatedByUser() { + return createdByUser; + } + + ZonedDateTime getCreationTime() { + return creationTime; + } + + String getDescription() { + return description; + } + + String getModifiedByUser() { + return modifiedByUser; + } + + ZonedDateTime getModificationTime() { + return modificationTime; + } + + String getTitle() { + return title; + } + + long getVersion() { + return version; + } + + void update(String newTitle, String newDescription) { + requireValidTitleAndDescription(newTitle, newDescription); + + this.title = newTitle; + this.description = newDescription; + } + + private void requireValidTitleAndDescription(String title, String description) { + notNull(title, "Title cannot be null."); + notEmpty(title, "Title cannot be empty."); + isTrue(title.length() <= MAX_LENGTH_TITLE, + "The maximum length of the title is <%d> characters.", + MAX_LENGTH_TITLE + ); + + isTrue((description == null) || (description.length() <= MAX_LENGTH_DESCRIPTION), + "The maximum length of the description is <%d> characters.", + MAX_LENGTH_DESCRIPTION + ); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .append("version", this.version) + .toString(); + } + + /** + * This entity is so simple that you don't really need to use the builder pattern + * (use a constructor instead). I use the builder pattern here because it makes + * the code a bit more easier to read. + */ + static class Builder { + private String description; + private String title; + + private Builder() {} + + Builder description(String description) { + this.description = description; + return this; + } + + Builder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = new Todo(this); + + build.requireValidTitleAndDescription(build.getTitle(), build.getDescription()); + + return build; + } + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java new file mode 100644 index 0000000..9e6fc09 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java @@ -0,0 +1,49 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +public interface TodoCrudService { + + /** + * Creates a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + TodoDTO create(TodoDTO newTodoEntry); + + /** + * Deletes a todo entry from the database. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + TodoDTO delete(Long id); + + /** + * Finds all todo entries that are saved to the database. + * @return + */ + List findAll(); + + /** + * Finds a todo entry by using the id given as a method parameter. + * @param id The id of the wanted todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO findById(Long id); + + /** + * Updates the information of an existing information. + * @param updatedTodoEntry The new information of an existing todo entry. + * @return The information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO update(TodoDTO updatedTodoEntry); +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java new file mode 100644 index 0000000..7eea8d2 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Size; +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTO { + + private String createdByUser; + + private ZonedDateTime creationTime; + + @Size(max = Todo.MAX_LENGTH_DESCRIPTION) + private String description; + + private Long id; + + private String modifiedByUser; + + private ZonedDateTime modificationTime; + + @NotEmpty + @Size(max = Todo.MAX_LENGTH_TITLE) + private String title; + + public TodoDTO() {} + + public String getCreatedByUser() { + return createdByUser; + } + + public ZonedDateTime getCreationTime() { + return creationTime; + } + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + public String getModifiedByUser() { + return modifiedByUser; + } + + public ZonedDateTime getModificationTime() { + return modificationTime; + } + + public String getTitle() { + return title; + } + + public void setCreatedByUser(String createdByUser) { + this.createdByUser = createdByUser; + } + + public void setCreationTime(ZonedDateTime creationTime) { + this.creationTime = creationTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setId(Long id) { + this.id = id; + } + + public void setModifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + + public void setModificationTime(ZonedDateTime modificationTime) { + this.modificationTime = modificationTime; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .toString(); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java new file mode 100644 index 0000000..aa36e28 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Petri Kainulainen + */ +final class TodoMapper { + + static List mapEntitiesIntoDTOs(List entities) { + return entities.stream() + .map(TodoMapper::mapEntityIntoDTO) + .collect(toList()); + } + + static TodoDTO mapEntityIntoDTO(Todo entity) { + TodoDTO dto = new TodoDTO(); + + dto.setCreatedByUser(entity.getCreatedByUser()); + dto.setCreationTime(entity.getCreationTime()); + dto.setDescription(entity.getDescription()); + dto.setId(entity.getId()); + dto.setModifiedByUser(entity.getModifiedByUser()); + dto.setModificationTime(entity.getModificationTime()); + dto.setTitle(entity.getTitle()); + + return dto; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java new file mode 100644 index 0000000..63f6948 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * This exception is thrown when a todo entry is not found by + * using the given id. + * + * @author Petri Kainulainen + */ +public class TodoNotFoundException extends RuntimeException { + + private final Long id; + + public TodoNotFoundException(Long id) { + super(); + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java new file mode 100644 index 0000000..25b26a9 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import net.petrikainulainen.springdata.jpa.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * This repository provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +interface TodoRepository extends BaseRepository { + + List findAll(); + + /** + * Finds todo entries by using the search term given as a method parameter. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. + */ + @Query("SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC") + List findBySearchTerm(@Param("searchTerm") String searchTerm); + + Optional findOne(Long id); + + void flush(); + + Todo save(Todo persisted); +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java new file mode 100644 index 0000000..63dcd6c --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * @author Petri Kainulainen + */ +public final class TodoSearchResultDTO { + + private Long id; + + private String title; + + public TodoSearchResultDTO() {} + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setId(Long id) { + this.id = id; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java new file mode 100644 index 0000000..a968085 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java @@ -0,0 +1,19 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides finder methods for {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +public interface TodoSearchService { + + /** + * Finds todo entries whose title or description contains the given search term. + * This search is case insensitive. + * @param searchTerm The search term. + * @return A list of todo entries whose title or description contains the given search term. + */ + List findBySearchTerm(String searchTerm); +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java new file mode 100644 index 0000000..5205657 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java @@ -0,0 +1,116 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * This controller provides the public API that is used to perform + * CRUD operations for todo entries. + * + * @author Petri Kainulainen + */ +@RestController +@RequestMapping("/api/todo") +final class TodoController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoController.class); + + private final TodoCrudService crudService; + + @Autowired + TodoController(TodoCrudService crudService) { + this.crudService = crudService; + } + + /** + * Create a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + TodoDTO create(@RequestBody @Valid TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + TodoDTO created = crudService.create(newTodoEntry); + LOGGER.info("Created a new todo entry: {}", created); + + return created; + } + + /** + * Deletes a todo entry. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public TodoDTO delete(@PathVariable("id") Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + TodoDTO deleted = crudService.delete(id); + LOGGER.info("Deleted the todo entry: {}", deleted); + + return deleted; + } + + /** + * Finds all todo entries. + * + * @return The information of all todo entries. + */ + @RequestMapping(method = RequestMethod.GET) + List findAll() { + LOGGER.info("Finding all todo entries"); + + List todoEntries = crudService.findAll(); + LOGGER.info("Found {} todo entries.", todoEntries.size()); + + return todoEntries; + } + + /** + * Finds a single todo entry. + * @param id The id of the requested todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.GET) + TodoDTO findById(@PathVariable("id") Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + TodoDTO todoEntry = crudService.findById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return todoEntry; + } + + /** + * Updates the information of an existing todo entry. + * @param updatedTodoEntry The new information of the updated todo entry. + * @return The updated information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.PUT) + TodoDTO update(@RequestBody @Valid TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + TodoDTO updated = crudService.update(updatedTodoEntry); + LOGGER.info("Updated the information of the todo entrY: {}", updated); + + return updated; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java new file mode 100644 index 0000000..6eede01 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * This controller provides the public API that is used to find todo entries by using + * different search criteria. + * + * @author Petri Kainulainen + */ +@RestController +final class TodoSearchController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoSearchController.class); + + private final TodoSearchService searchService; + + @Autowired + public TodoSearchController(TodoSearchService searchService) { + this.searchService = searchService; + } + + /** + * Finds todo entries whose title or description contains the given search term. This + * search is case insensitive. + * @param searchTerm The used search term. + * @return + */ + @RequestMapping(value = "/api/todo/search", method = RequestMethod.GET) + public List findBySearchTerm(@RequestParam("searchTerm") String searchTerm) { + LOGGER.info("Finding todo entries by search term: {}", searchTerm); + + List searchResults = searchService.findBySearchTerm(searchTerm); + LOGGER.info("Found {} todo entries", searchResults.size()); + + return searchResults; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java new file mode 100644 index 0000000..b02059e --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of an error that occurred when the API tried + * to perform the operation requested by the client. + * + * @author Petri Kainulainen + */ +final class ErrorDTO { + + private final String code; + private final String message; + + ErrorDTO(String code, String message) { + notNull(code, "Code cannot be null."); + notEmpty(code, "Code cannot be empty."); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty"); + + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java new file mode 100644 index 0000000..44234a5 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of a single field error. + * + * @author Petri Kainulainen + */ +final class FieldErrorDTO { + + private final String field; + + private final String message; + + FieldErrorDTO(String field, String message) { + notNull(field, "Field cannot be null."); + notEmpty(field, "Field cannot be empty"); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty."); + + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java new file mode 100644 index 0000000..5ad9e9b --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; +import java.util.Locale; + +/** + * This class handles the exceptions thrown by our REST API. + * + * @author Petri Kainulainen + */ +@ControllerAdvice +public final class RestErrorHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class); + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + + private final MessageSource messageSource; + + @Autowired + public RestErrorHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * Processes an error that occurs when the requested todo entry is not found. + * @param ex The exception that was thrown when the todo entry was not found. + * @param currentLocale The current locale. + * @return An error object that contains the error code and message. + */ + @ExceptionHandler(TodoNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + ErrorDTO handleTodoEntryNotFound(TodoNotFoundException ex, Locale currentLocale) { + LOGGER.error("Todo entry was not found by using id: {}", ex.getId()); + + MessageSourceResolvable errorMessageRequest = createSingleErrorMessageRequest( + ERROR_CODE_TODO_ENTRY_NOT_FOUND, + ex.getId() + ); + + String errorMessage = messageSource.getMessage(errorMessageRequest, currentLocale); + return new ErrorDTO(HttpStatus.NOT_FOUND.name(), errorMessage); + } + + private DefaultMessageSourceResolvable createSingleErrorMessageRequest(String errorMessageCode, Object... params) { + return new DefaultMessageSourceResolvable(new String[] {errorMessageCode}, params); + } + + /** + * Processes an error that occurs when the validation of an object fails. + * + * @param ex The exception that was thrown when the validation failed. + * @return An error object that describes all validation errors. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ValidationErrorDTO handleValidationErrors(MethodArgumentNotValidException ex, Locale currentLocale) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result.getFieldErrors(); + LOGGER.error("Found {} validation errors", fieldErrors.size()); + + return constructValidationErrors(fieldErrors, currentLocale); + } + + private ValidationErrorDTO constructValidationErrors(List fieldErrors, Locale currentLocale) { + ValidationErrorDTO dto = new ValidationErrorDTO(); + + for (FieldError fieldError: fieldErrors) { + String localizedErrorMessage = getValidationErrorMessage(fieldError, currentLocale); + dto.addFieldError(fieldError.getField(), localizedErrorMessage); + } + + return dto; + } + + private String getValidationErrorMessage(FieldError fieldError, Locale currentLocale) { + String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale); + + //If the message was not found, return the most accurate field error code instead. + //You can remove this check if you prefer to get the default error message. + if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) { + String[] fieldErrorCodes = fieldError.getCodes(); + localizedErrorMessage = fieldErrorCodes[0]; + } + + return localizedErrorMessage; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java new file mode 100644 index 0000000..8355c7b --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import org.springframework.http.HttpStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class contains the information of validation errors that are found + * from a controller method parameter that is annotated with the + * {@link javax.validation.Valid} annotation. + * + * @author Petri Kainulainen + */ +final class ValidationErrorDTO { + + private final String code = HttpStatus.BAD_REQUEST.name(); + + private final List fieldErrors = new ArrayList<>(); + + ValidationErrorDTO() { + } + + void addFieldError(String field, String message) { + FieldErrorDTO error = new FieldErrorDTO(field, message); + fieldErrors.add(error); + } + + public String getCode() { + return code; + } + + public List getFieldErrors() { + return fieldErrors; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java new file mode 100644 index 0000000..141a948 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java @@ -0,0 +1,46 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This filter reads the {@link org.springframework.security.web.csrf.CsrfToken} from the {@link HttpServletRequest} and + * sets its content to the {@link HttpServletResponse} headers. + * + * I borrowed this idea from this StackOverflow question. + * + * @author Petri Kainulainen + */ +public class CsrfHeaderFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(CsrfHeaderFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + LOGGER.trace("Reading CSRF token from the request."); + + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (token != null) { + LOGGER.trace("CSRF token was found. Creating HTTP response headers."); + response.setHeader("X-CSRF-HEADER", token.getHeaderName()); + response.setHeader("X-CSRF-PARAM", token.getParameterName()); + response.setHeader("X-CSRF-TOKEN", token.getToken()); + } + else { + LOGGER.trace("CSRF Token was not found. Doing nothing."); + } + + filterChain.doFilter(request, response); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java new file mode 100644 index 0000000..f6e70cb --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Petri Kainulainen + */ +@RestController +public class CsrfTokenController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsrfTokenController.class); + + @RequestMapping(value = "/api/csrf", method = RequestMethod.HEAD) + public void getCsrfToken() { + LOGGER.info("Getting CSRF token."); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..887e25b --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication entry point returns the HTTP status code 401. + * @author Petri Kainulainen + */ +public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + LOGGER.info("Authentication required. Returning HTTP status code 401."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..daf635b --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication failure handler returns the HTTP status code 403. + * @author Petri Kainulainen + */ +public final class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationFailureHandler.class); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + LOGGER.info("Authentication failed with message: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failed."); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ff84785 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication success handler returns the information of the authenticated + * user as JSON. + * + * @author Petri Kainulainen + */ +public final class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + LOGGER.info("Authentication was successful"); + response.sendRedirect(response.encodeRedirectURL("/api/authenticated-user")); + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java new file mode 100644 index 0000000..ef7959d --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java @@ -0,0 +1,45 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to return the information + * of the authenticated user. + * + * @author Petri Kainulainen + */ +@RestController +final class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + /** + * Returns the information of the authenticated user as JSON. The returned information + * contains the username and the user role of the authenticated user. + * + * @param authenticatedUser The information of the authenticated user. + * @return + */ + @RequestMapping(value = "/api/authenticated-user", method = RequestMethod.GET) + public UserDTO getAuthenticatedUser(@AuthenticationPrincipal User authenticatedUser) { + LOGGER.info("Getting authenticated user."); + + if (authenticatedUser == null) { + //If anonymous users can access this controller method, someone has changed + //the security configuration and it must be fixed. + LOGGER.error("Authenticated user is not found."); + throw new AccessDeniedException("Anonymous users cannot request the information of the authenticated user."); + } + else { + LOGGER.info("User with username: {} is authenticated", authenticatedUser.getUsername()); + return new UserDTO(authenticatedUser.getUsername(), authenticatedUser.getAuthorities()); + } + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java new file mode 100644 index 0000000..92b99ed --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import net.petrikainulainen.springdata.jpa.common.PreCondition; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * This class contains the information of the authenticated user. + * + * @author Petri Kainulainen + */ +public final class UserDTO { + + private final String username; + + private final UserRole role; + + UserDTO(String username, Collection authorities) { + PreCondition.isTrue(!username.isEmpty(), "Username cannot be empty."); + PreCondition.isTrue(authorities.size() == 1, "User must have only one granted authority."); + this.username = username; + + GrantedAuthority authority = authorities.iterator().next(); + this.role = UserRole.valueOf(authority.getAuthority()); + } + + public String getUsername() { + return username; + } + + public UserRole getRole() { + return role; + } +} diff --git a/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java new file mode 100644 index 0000000..8b3e6a6 --- /dev/null +++ b/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java @@ -0,0 +1,8 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +/** + * @author Petri Kainulainen + */ +enum UserRole { + ROLE_USER +} diff --git a/custom-method-all-repos/src/main/resources/META-INF/jpa-named-queries.properties b/custom-method-all-repos/src/main/resources/META-INF/jpa-named-queries.properties new file mode 100644 index 0000000..97d737e --- /dev/null +++ b/custom-method-all-repos/src/main/resources/META-INF/jpa-named-queries.properties @@ -0,0 +1,2 @@ +Todo.findBySearchTermNamedFile=SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC +Todo.findBySearchTermNamedNativeFile=SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/META-INF/orm.xml b/custom-method-all-repos/src/main/resources/META-INF/orm.xml new file mode 100644 index 0000000..cc2bf80 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/META-INF/orm.xml @@ -0,0 +1,16 @@ + + + + + SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC + + + + SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/application.properties b/custom-method-all-repos/src/main/resources/application.properties new file mode 100644 index 0000000..7ac8298 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/application.properties @@ -0,0 +1,12 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/applicationContext-persistence.xml b/custom-method-all-repos/src/main/resources/applicationContext-persistence.xml new file mode 100644 index 0000000..f143e51 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/applicationContext-persistence.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${hibernate.dialect} + + + ${hibernate.hbm2ddl.auto} + + + ${hibernate.ejb.naming_strategy} + + + ${hibernate.show_sql} + + + ${hibernate.format_sql} + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/applicationContext-web.xml b/custom-method-all-repos/src/main/resources/applicationContext-web.xml new file mode 100644 index 0000000..db48af6 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/applicationContext-web.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + WRITE_DATES_AS_TIMESTAMPS + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/applicationContext.xml b/custom-method-all-repos/src/main/resources/applicationContext.xml new file mode 100644 index 0000000..b9ee424 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/applicationContext.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/i18n/messages.properties b/custom-method-all-repos/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..0e376f5 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/i18n/messages.properties @@ -0,0 +1,5 @@ +error.todo.entry.not.found=No todo entry was found by using id: {0} + +NotEmpty.todoDTO.title=The title cannot be empty +Size.todoDTO.description=The maximum length of description is 500 characters +Size.todoDTO.title=The maximum length of title is 100 characters \ No newline at end of file diff --git a/custom-method-all-repos/src/main/resources/integration-test.properties b/custom-method-all-repos/src/main/resources/integration-test.properties new file mode 100644 index 0000000..3605c55 --- /dev/null +++ b/custom-method-all-repos/src/main/resources/integration-test.properties @@ -0,0 +1,14 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa;DB_CLOSE_ON_EXIT=FALSE +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false + +test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1 \ No newline at end of file diff --git a/tutorial-part-five/src/main/resources/log4j.properties b/custom-method-all-repos/src/main/resources/log4j.properties similarity index 75% rename from tutorial-part-five/src/main/resources/log4j.properties rename to custom-method-all-repos/src/main/resources/log4j.properties index 5ad34eb..668d97a 100644 --- a/tutorial-part-five/src/main/resources/log4j.properties +++ b/custom-method-all-repos/src/main/resources/log4j.properties @@ -3,4 +3,6 @@ log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG + +log4j.logger.org.hibernate=INFO +log4j.logger.org.springframework=INFO \ No newline at end of file diff --git a/custom-method-all-repos/src/main/webapp/WEB-INF/jsp/frontend/client.jsp b/custom-method-all-repos/src/main/webapp/WEB-INF/jsp/frontend/client.jsp new file mode 100644 index 0000000..84158d0 --- /dev/null +++ b/custom-method-all-repos/src/main/webapp/WEB-INF/jsp/frontend/client.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ +
+
+

+

+
+
+ + + diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java new file mode 100644 index 0000000..7e90183 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Petri Kainulainen + */ +public class PreConditionTest { + + private static final String STATIC_ERROR_MESSAGE = "static error message"; + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, "Dynamic error message with parameter: %d", 1L); + } + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic error message with parameter: 1"); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, STATIC_ERROR_MESSAGE); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsNotEmpty_ShouldNotThrowException() { + PreCondition.notEmpty(" ", STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsEmpty_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNotNull_ShouldNotThrowException() { + PreCondition.notNull(new Object(), STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNull_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java new file mode 100644 index 0000000..ba828f8 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java @@ -0,0 +1,96 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoSearchServiceTest { + + private static final String SEARCH_TERM = "itl"; + + private TodoRepository repository; + private RepositoryTodoSearchService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoSearchService(repository); + } + + public class FindBySearchTerm { + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + given(repository.findBySearchTerm(SEARCH_TERM)).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List searchResults = service.findBySearchTerm(SEARCH_TERM); + assertThat(searchResults).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final Long ID = 20L; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findBySearchTerm(SEARCH_TERM)).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List searchResults = service.findBySearchTerm(SEARCH_TERM); + assertThat(searchResults).hasSize(1); + } + + @Test + public void shouldReturnTheInformationOfOneTodoEntry() { + TodoDTO found = service.findBySearchTerm(SEARCH_TERM).get(0); + + assertThatTodoDTO(found) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java new file mode 100644 index 0000000..495a420 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java @@ -0,0 +1,379 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoServiceTest { + + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + private static final Long ID = 20L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + private TodoRepository repository; + + private RepositoryTodoService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoService(repository); + } + + public class Create { + + @Before + public void returnNewTodoEntry() { + given(repository.save(isA(Todo.class))).willAnswer( + invocationOnMock -> new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build() + ); + } + + @Test + public void shouldPersistNewTodoEntryWithCorrectInformation() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + service.create(newTodoEntry); + + verify(repository, times(1)).save( + assertArg(persisted -> assertThatTodoEntry(persisted) + .hasNoCreationAuditFieldValues() + .hasDescription(DESCRIPTION) + .hasNoId() + .hasNoModificationAuditFieldValues() + .hasTitle(TITLE) + ) + ); + verifyNoMoreInteractions(repository); + } + + @Test + public void shouldReturnTheInformationOfPersistedTodoEntry() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + TodoDTO created = service.create(newTodoEntry); + assertThatTodoDTO(created) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.deleteById(ID)).willReturn(Optional.empty()); + + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.delete(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException ex = (TodoNotFoundException) thrown; + assertThat(ex.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + private Todo deleted; + + @Before + public void returnDeletedTodoEntry() { + deleted = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.deleteById(ID)).willReturn(Optional.of(deleted)); + } + + @Test + public void shouldDeleteFoundTodoEntry() { + service.delete(ID); + + verify(repository, times(1)).deleteById(ID); + } + + @Test + public void shouldReturnTheInformationOfDeletedTodoEntry() { + TodoDTO deleted = service.delete(ID); + + assertThatTodoDTO(deleted) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindAll { + + public class WhenNoTodoEntryAreFound { + + @Before + public void returnNoTodoEntries() { + given(repository.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).hasSize(1); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO todoEntry = service.findAll().get(0); + + assertThatTodoDTO(todoEntry) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindOne { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.findById(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(found)); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO returned = service.findById(ID); + + assertThatTodoDTO(returned) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + Throwable thrown = catchThrowable(() -> service.update(updatedTodoEntry)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + private Todo updated; + + @Before + public void returnUpdatedTodoEntry() { + updated = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(updated)); + } + + @Test + public void shouldUpdateTitleAndDescription() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldNotUpdateIdOrAuditInformation() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasId(ID) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntry() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + TodoDTO returnedTodoEntry = service.update(updatedTodoEntry); + + assertThatTodoDTO(returnedTodoEntry) + .hasDescription(UPDATED_DESCRIPTION) + .hasId(ID) + .hasTitle(UPDATED_TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java new file mode 100644 index 0000000..ed98667 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author Petri Kainulainen + */ +public final class TestUtil { + + private TestUtil() {} + + public static String createStringWithLength(int length) { + StringBuilder string = new StringBuilder(); + + for (int index = 0; index < length; index++) { + string.append("a"); + } + + return string.toString(); + } + + public static ZonedDateTime parseDateTime(String dateAndTime) { + return ZonedDateTime.parse(dateAndTime, DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java new file mode 100644 index 0000000..e88f27c --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java @@ -0,0 +1,198 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This class provides a fluent API that can be used for writing assertions + * to {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +final class TodoAssert extends AbstractAssert { + + private TodoAssert(Todo actual) { + super(actual, TodoAssert.class); + } + + static TodoAssert assertThatTodoEntry(Todo actual) { + return new TodoAssert(actual); + } + + TodoAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage(String.format( + "Expected description to be <%s> but was <%s>.", + expectedDescription, + actualDescription + )) + .isEqualTo(expectedDescription); + + return this; + } + + TodoAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creationTime to be but was <%s>", + actualCreationTime + ) + .isNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + return this; + } + + TodoAssert hasNoDescription() { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage("Expected description to be but was <%s>", actualDescription) + .isNull(); + + return this; + } + + TodoAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be <%d> but was <%d>", + expectedId, + actualId + ) + .isEqualTo(expectedId); + + return this; + } + + TodoAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>.", actualId) + .isNull(); + + return this; + } + + TodoAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>.", + actualModificationTime + ) + .isNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>", + actualModificationTime + ) + .isNull(); + + return this; + } + + TodoAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>.", + expectedTitle, + actualTitle + ) + .isEqualTo(actualTitle); + + return this; + } + + public TodoAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java new file mode 100644 index 0000000..90ee955 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java @@ -0,0 +1,71 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +class TodoBuilder { + + private Long id; + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + TodoBuilder() {} + + TodoBuilder id(Long id) { + this.id = id; + return this; + } + + TodoBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + TodoBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + TodoBuilder description(String description) { + this.description = description; + return this; + } + + TodoBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + TodoBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + TodoBuilder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = Todo.getBuilder() + .title(title) + .description(description) + .build(); + + ReflectionTestUtils.setField(build, "createdByUser", createdByUser); + ReflectionTestUtils.setField(build, "creationTime", creationTime); + ReflectionTestUtils.setField(build, "id", id); + ReflectionTestUtils.setField(build, "modifiedByUser", modifiedByUser); + ReflectionTestUtils.setField(build, "modificationTime", modificationTime); + + return build; + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java new file mode 100644 index 0000000..462d90d --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java @@ -0,0 +1,179 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTOAssert extends AbstractAssert { + + private TodoDTOAssert(TodoDTO actual) { + super(actual, TodoDTOAssert.class); + } + + public static TodoDTOAssert assertThatTodoDTO(TodoDTO actual) { + return new TodoDTOAssert(actual); + } + + public TodoDTOAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage( + "Expected description to be <%s> but was <%s>", + expectedDescription, + actualDescription + ) + .isEqualTo(expectedDescription); + + return this; + } + + public TodoDTOAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage( + "Expected id to be <%d> but was <%d>", + actualId, + expectedId + ) + .isEqualTo(expectedId); + + return this; + } + + public TodoDTOAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage("Expected creationTime to be but was <%s>", actualCreationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>", actualId) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be but was <%s>", + actualModifiedByUser + ) + .isNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage("Expected modification time to be but was <%d>", actualModificationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>", + expectedTitle, + actualTitle + ) + .isEqualTo(expectedTitle); + + return this; + } + + public TodoDTOAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoDTOAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoDTOAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoDTOAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java new file mode 100644 index 0000000..e0b5505 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java @@ -0,0 +1,68 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public class TodoDTOBuilder { + + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private Long id; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + public TodoDTOBuilder() {} + + public TodoDTOBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + public TodoDTOBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + public TodoDTOBuilder description(String description) { + this.description = description; + return this; + } + + public TodoDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public TodoDTOBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + public TodoDTOBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + public TodoDTOBuilder title(String title) { + this.title = title; + return this; + } + + public TodoDTO build() { + TodoDTO build = new TodoDTO(); + + build.setCreatedByUser(createdByUser); + build.setCreationTime(creationTime); + build.setDescription(description); + build.setId(id); + build.setModifiedByUser(modifiedByUser); + build.setModificationTime(modificationTime); + build.setTitle(title); + + return build; + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java new file mode 100644 index 0000000..c5ff69d --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java @@ -0,0 +1,340 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoTest { + + private static final int MAX_LENGTH_DESCRIPTION = 500; + private static final int MAX_LENGTH_TITLE = 100; + + private static final String DESCRIPTION = "description"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + public class Build { + + public class WhenTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(null) + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title("") + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(tooLongTitle) + .description(DESCRIPTION) + .build(); + } + } + } + + public class WhenDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(TITLE) + .description(tooLongDescription) + .build(); + } + } + + public class WhenTitleAndDescriptionAreValid { + + @Test + public void shouldNotSetId() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoId(); + } + + @Test + public void shouldNotSetCreationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoCreationAuditFieldValues(); + } + + @Test + public void shouldNotSetModificationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoModificationAuditFieldValues(); + } + + @Test + public void shouldSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasDescription(DESCRIPTION); + } + + @Test + public void shouldSetTitle() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(TITLE); + } + + public class WhenMaxLengthTitleIsGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldCreateNewObjectAndSetTitle() { + Todo build = Todo.getBuilder() + .title(maxLengthTitle) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(maxLengthTitle); + } + } + + public class WhenMaxLengthDescriptionIsGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + + } + + @Test + public void shouldCreateNewObjectAndSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(maxLengthDescription) + .build(); + + assertThatTodoEntry(build) + .hasDescription(maxLengthDescription); + } + } + + public class WhenNoDescriptionIsGiven { + + @Test + public void shouldCreateNewObjectWithoutDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .build(); + + assertThatTodoEntry(build) + .hasNoDescription(); + } + } + } + } + + public class Update { + + private Todo updated; + + @Before + public void createUpdatedTodoEntry() { + updated = Todo.getBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + } + + public class WhenNewTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + updated.update(null, UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update("", UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(tooLongTitle, UPDATED_DESCRIPTION); + } + } + } + + public class WhenNewDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(UPDATED_TITLE, tooLongDescription); + } + } + + public class WhenNewTitleAndNewDescriptionAreValid { + + public class WhenMaxLengthTitleAndNewDescriptionAreGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldUpdateTitle() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasTitle(maxLengthTitle); + } + + @Test + public void shouldUpdateDescription() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION); + } + } + + public class WhenNewTitleIsGivenAndNewDescriptionIsNull { + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldRemoveDescription() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasNoDescription(); + } + } + + public class WhenNewTitleAndMaxLengthDescriptionAreGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + } + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldUpdateDescription() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasDescription(maxLengthDescription); + } + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java new file mode 100644 index 0000000..a0dd3eb --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java @@ -0,0 +1,691 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoControllerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + + private static final String ERROR_MESSAGE_KEY_MISSING_TITLE = "NotEmpty.todoDTO.title"; + private static final String ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION = "Size.todoDTO.description"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_TITLE = "Size.todoDTO.title"; + + private static final Long ID = 1L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private static final String TITLE = "title"; + + private MockMvc mockMvc; + + private TodoCrudService crudService; + + private StaticMessageSource messageSource; + + @Before + public void setUp() { + crudService = mock(TodoCrudService.class); + + messageSource = new StaticMessageSource(); + messageSource.setUseCodeAsDefaultMessage(true); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(crudService)) + .setHandlerExceptionResolvers(WebTestConfig.restErrorHandler(messageSource)) + .setLocaleResolver(WebTestConfig.fixedLocaleResolver(CURRENT_LOCALE)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setValidator(WebTestConfig.validator()) + .build(); + } + + public class Create { + + public class WhenTodoEntryIsNotValid { + + public class WhenTodoEntryIsEmpty { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + private TodoDTO newTodoEntry; + + @Before + public void createInputAndReturnNewTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + newTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .title(maxLengthTitle) + .build(); + + TodoDTO created = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.create(isA(TodoDTO.class))).willReturn(created); + } + + @Test + public void shouldReturnResponseStatusCreated() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isCreated()); + } + + @Test + public void shouldReturnCreatedTodoEntryAsJson() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldCreateNewTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verify(crudService, times(1)).create( + assertArg(created -> assertThatTodoDTO(created) + .hasDescription(maxLengthDescription) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoId() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwNotFoundException() { + given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnDeletedTodoEntry() { + TodoDTO deleted = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.delete(ID)).willReturn(deleted); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class FindAll { + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnNoTodoEntries() { + given(crudService.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } + + public class FindById { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findById(ID)).willReturn(found); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + public class WhenTodoEntryIsNotValid { + + public class WhenTitleAndDescriptionAreMissing { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + TodoDTO updatedTodoEntry; + + @Before + public void createInputAndReturnUpdatedTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + updatedTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .id(ID) + .title(maxLengthTitle) + .build(); + + TodoDTO updated = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.update(isA(TodoDTO.class))).willReturn(updated); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldUpdateTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verify(crudService, times(1)).update( + assertArg(updated -> assertThatTodoDTO(updated) + .hasDescription(maxLengthDescription) + .hasId(ID) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java new file mode 100644 index 0000000..6e4cc4a --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java @@ -0,0 +1,128 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoSearchControllerTest { + + private static final String SEARCH_TERM = "itl"; + + private MockMvc mockMvc; + + private TodoSearchService searchService; + + @Before + public void setUp() { + searchService = mock(TodoSearchService.class); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoSearchController(searchService)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setCustomArgumentResolvers(WebTestConfig.sortArgumentResolver()) + .build(); + } + + public class FindBySearchTerm { + + @Test + public void shouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldPassSearchTermForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ); + + verify(searchService, times(1)).findBySearchTerm(eq(SEARCH_TERM)); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + given(searchService.findBySearchTerm(SEARCH_TERM)).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + private final Long ID= 1L; + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(searchService.findBySearchTerm(SEARCH_TERM)).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java new file mode 100644 index 0000000..a099861 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java @@ -0,0 +1,111 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import net.petrikainulainen.springdata.jpa.web.error.RestErrorHandler; +import org.springframework.context.MessageSource; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.FixedLocaleResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Locale; + +/** + * This factory class provides methods that can be used to create objects that are useful + * when we are writing unit tests for our controller methods by using the Spring MVC Test + * framework. + * + * @author Petri Kainulainen + */ +final class WebTestConfig { + + private WebTestConfig() {} + + /** + * Configures a {@link org.springframework.web.servlet.LocaleResolver} that always returns the + * configured {@link java.util.Locale}. + * + * @return + */ + static LocaleResolver fixedLocaleResolver(Locale fixedLocale) { + return new FixedLocaleResolver(fixedLocale); + } + + /** + * This method creates a custom {@link org.springframework.http.converter.HttpMessageConverter} which ensures that: + * + *
    + *
  • Null values are ignored.
  • + *
  • + * The new Java 8 date objects are serialized in standard + * ISO-8601 string representation. + *
  • + *
+ * + * @return + */ + static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return converter; + } + + /** + * This method ensures that the {@link RestErrorHandler} class + * is used to handle the exceptions thrown by the tested controller. I borrowed this idea from + * this StackOverflow answer. + * + * @return an error handler component that delegates relevant exceptions forward to the {@link RestErrorHandler} class. + */ + static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) { + final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() { + @Override + protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerMethod handlerMethod, + final Exception exception) { + Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception); + if (method != null) { + return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method); + } + return super.getExceptionHandlerMethod(handlerMethod, exception); + } + }; + exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter())); + exceptionResolver.afterPropertiesSet(); + return exceptionResolver; + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Sort} objects by using the request params of the + * incoming request. + * @return + */ + static SortHandlerMethodArgumentResolver sortArgumentResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + /** + * This method creates a validator object that adds support for bean validation API 1.0 and 1.1. + * + * @return The created validator object. + */ + static LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java new file mode 100644 index 0000000..2246b2b --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java @@ -0,0 +1,32 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.http.MediaType; + +import java.nio.charset.Charset; + +/** + * @author Petri Kainulainen + */ +public final class WebTestConstants { + + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), + MediaType.APPLICATION_JSON.getSubtype(), + Charset.forName("utf8") + ); + + static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + static final String ERROR_CODE_VALIDATION_FAILED = "BAD_REQUEST"; + + static final String FIELD_NAME_DESCRIPTION = "description"; + static final String FIELD_NAME_TITLE = "title"; + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + static final String REQUEST_PARAM_SEARCH_TERM = "searchTerm"; + + /** + * Prevents instantiation. + */ + private WebTestConstants() {} +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java new file mode 100644 index 0000000..9340fe6 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +/** + * @author Petri Kainulainen + */ +final class WebTestUtil { + + /** + * Prevents instantiation + */ + private WebTestUtil() {} + + /** + * Transforms an object into JSON and returns the JSON as a byte array. + * @param object The object that is transformed into JSON. + * @return The JSON representation of an object as a byte array. + * @throws IOException + */ + static byte[] convertObjectToJsonBytes(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsBytes(object); + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java new file mode 100644 index 0000000..528a552 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.ErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ErrorDTOTest { + + private static final String CODE = "code"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenCodeIsInvalid { + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenCodeIsNull() { + new ErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenCodeIsEmpty() { + new ErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new ErrorDTO(CODE, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new ErrorDTO(CODE, ""); + } + } + + public class WhenCodeAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetCode() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getCode()).isEqualTo(CODE); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java new file mode 100644 index 0000000..25fc6bf --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java @@ -0,0 +1,64 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class FieldErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenFieldIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenFieldIsNull() { + new FieldErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenFieldIsEmpty() { + new FieldErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new FieldErrorDTO(FIELD, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new FieldErrorDTO(FIELD, ""); + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetField() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java new file mode 100644 index 0000000..5ff8f34 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java @@ -0,0 +1,242 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.MethodParameter; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.List; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RestErrorHandlerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + + private static final Long TODO_ID = 99L; + + private MessageSource messageSource; + + private RestErrorHandler errorHandler; + + @Before + public void setUp() { + messageSource = mock(MessageSource.class); + this.errorHandler = new RestErrorHandler(messageSource); + } + + public class HandleTodoEntryNotFound { + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + + private static final String ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 99"; + + @Before + public void returnErrorMessageNotFound() { + given(messageSource.getMessage( + isA(MessageSourceResolvable.class), + isA(Locale.class)) + ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldFindErrorMessageByUsingCurrentLocale() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage(isA(MessageSourceResolvable.class), eq(CURRENT_LOCALE)); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectId() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getArguments()) + .containsOnly(TODO_ID) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectMessageCode() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getCodes()) + .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldReturnErrorThatHasCorrectErrorCode() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getCode()).isEqualTo(ERROR_CODE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldReturnErrorThatHasCorrectMessage() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getMessage()).isEqualTo(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + } + + public class HandleValidationErrors { + + private static final String ERROR_CODE_VALIDATION_ERROR = "BAD_REQUEST"; + private static final String ERROR_MESSAGE_VALIDATION_ERROR = "validationError"; + + private static final String FIELD_DEFAULT_MESSAGE = "DefaultMessage"; + private static final String FIELD_WITH_VALIDATION_ERROR = "field"; + private static final String OBJECT_WITH_VALIDATION_ERROR = "todoDTO"; + + private static final String VALIDATION_ERROR_CODE_ACCURATE = "Error"; + private static final String VALIDATION_ERROR_CODE_LESS_ACCURATE = "Maybe"; + + public class WhenOneValidationErrorIsFound { + + public class WhenMessageIsFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(ERROR_MESSAGE_VALIDATION_ERROR); + } + } + + public class WhenMessageIsNotFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnDefaultErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasFieldErrorWithMostAccurateFieldErrorCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(VALIDATION_ERROR_CODE_ACCURATE); + } + } + } + + private MethodArgumentNotValidException createExceptionWithFieldErrors(FieldError... fieldErrors) { + BindingResult bindingResult = new BeanPropertyBindingResult(new TodoDTO(), OBJECT_WITH_VALIDATION_ERROR); + + for (FieldError fieldError: fieldErrors) { + bindingResult.addError(fieldError); + } + + return new MethodArgumentNotValidException(mock(MethodParameter.class), bindingResult); + } + + + private final class FieldErrorBuilder { + + private String defaultMessage; + private String[] errorCodes; + private String fieldName; + + private FieldErrorBuilder() {} + + private FieldErrorBuilder defaultMessage(String defaultMessage) { + this.defaultMessage = defaultMessage; + return this; + } + + private FieldErrorBuilder errorCodes(String... errorCodes) { + this.errorCodes = errorCodes; + return this; + } + + private FieldErrorBuilder fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + private FieldError build() { + return new FieldError(OBJECT_WITH_VALIDATION_ERROR, + fieldName, + null, + false, + errorCodes, + new Object[]{}, + defaultMessage + ); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java new file mode 100644 index 0000000..8ae069a --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import net.petrikainulainen.springdata.jpa.web.error.ValidationErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ValidationErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class AddFieldError { + + public class WhenFieldIsInvalid { + + public class WhenFieldIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(null, MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(null, MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenFieldIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError("", MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError("", MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenMessageIsInvalid { + + public class WhenMessageIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, null); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, null)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenMessageIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, ""); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, "")); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewFieldErrorAndSetField() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewFieldErrorAndSetMessage() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java new file mode 100644 index 0000000..1928461 --- /dev/null +++ b/custom-method-all-repos/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class UserDTOTest { + + public class CreateNew { + + private final String ROLE_USER = UserRole.ROLE_USER.name(); + + public class WhenUsernameIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER); + new UserDTO("", authorities); + } + } + + public class WhenUserNameIsNotEmpty { + + private final String USERNAME = "username"; + + public class WhenUserHasNoGrantedAuthorities { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + new UserDTO(USERNAME, new ArrayList<>()); + } + } + + public class WhenUserHasOneGrantedAuthority { + + public class WhenGrantedAuthorityIsKnown { + + private Collection authorities; + + @Before + public void createKnownAuthority() { + authorities = createAuthorities(ROLE_USER); + } + + @Test + public void shouldSetUsername() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getUsername()).isEqualTo(USERNAME); + } + + @Test + public void shouldSetRole() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getRole()).isEqualTo(UserRole.ROLE_USER); + } + } + + public class WhenGrantedAuthorityIsUnknown { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities("UNKNOWN_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + + public class WhenUserHasMoreThanOneGrantedAuthority { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER, "ANOTHER_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + } + + private Collection createAuthorities(String... roles) { + List authorities = new ArrayList<>(); + + for (String role: roles) { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); + authorities.add(authority); + } + + return authorities; + } +} diff --git a/custom-method-single-repo/.gitignore b/custom-method-single-repo/.gitignore new file mode 100644 index 0000000..02895f1 --- /dev/null +++ b/custom-method-single-repo/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.gradle +.idea +*.iml +build +h2db +target +node_modules +bower_components +build \ No newline at end of file diff --git a/tutorial-part-one/LICENSE b/custom-method-single-repo/LICENSE similarity index 88% rename from tutorial-part-one/LICENSE rename to custom-method-single-repo/LICENSE index b333aa5..642bfb3 100644 --- a/tutorial-part-one/LICENSE +++ b/custom-method-single-repo/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011 Petri Kainulainen +Copyright 2014 Petri Kainulainen Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -10,4 +10,4 @@ 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. \ No newline at end of file +limitations under the License. diff --git a/custom-method-single-repo/README.md b/custom-method-single-repo/README.md new file mode 100644 index 0000000..9e29189 --- /dev/null +++ b/custom-method-single-repo/README.md @@ -0,0 +1,70 @@ +This blog post is the example application of the following blog posts: + +* [Spring Data JPA Tutorial: Getting the Required Dependencies](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-getting-the-required-dependencies/) +* [Spring Data JPA Tutorial: Configuration](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/) +* [Spring Data JPA Tutorial: CRUD](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/) +* [Spring Data JPA Tutorial: Auditing, Part One](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-one/) +* [Spring Data JPA Tutorial: Auditing, Part Two](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-two/) +* [Spring Data JPA Tutorial: Adding Custom Methods Into a Single Repository](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-eight-adding-functionality-to-a-repository/) + +Prerequisites +============= + +You need to install the following tools if you want to run this application: + +Backend +--------- + +* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* [Maven](http://maven.apache.org/) (the application is tested with Maven 3.2.1) + +Frontend +---------- + +* [Node.js](http://nodejs.org/) +* [NPM](https://www.npmjs.org/) +* [Bower](http://bower.io/) +* [Gulp](http://gulpjs.com/) + +You can install these tools by following these steps: + +1. Install Node.js by using a [downloaded binary](http://nodejs.org/download/) or a [package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). + You can also read this blog post: [How to install Node.js and NPM](http://blog.nodeknockout.com/post/65463770933/how-to-install-node-js-and-npm) + +2. Install Bower by using the following command: + + npm install -g bower + +3. Install Gulp by using the following command: + + npm install -g gulp + + +Running the Tests +================= + +You can run the unit tests by using the following command: + + mvn clean test -P dev + +You can run the integration tests by using the following command: + + mvn clean verify -P integration-test + +Running the Application +======================= + +You can run the application by using the following command: + + mvn clean jetty:run -P dev + +Credits +========= + +* Kyösti Herrala. The Gulp build script and its Maven integration are based on Kyösti's ideas. +* [Techniques for authentication in AngularJS applications](https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec) + +Known Issues +============ + +* If you refresh the login page, you aren't redirected away from it after successful login. \ No newline at end of file diff --git a/custom-method-single-repo/frontend/.bowerrc b/custom-method-single-repo/frontend/.bowerrc new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/custom-method-single-repo/frontend/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file diff --git a/custom-method-single-repo/frontend/.jshintrc b/custom-method-single-repo/frontend/.jshintrc new file mode 100644 index 0000000..f648d46 --- /dev/null +++ b/custom-method-single-repo/frontend/.jshintrc @@ -0,0 +1,33 @@ +{ + "globalstrict": true, + "browser": true, + "devel": true, + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "globals": { + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "angular": true, + "jQuery": true, + "_": true, + "$": true + } +} \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/app.js b/custom-method-single-repo/frontend/app/app.js new file mode 100644 index 0000000..1840cd6 --- /dev/null +++ b/custom-method-single-repo/frontend/app/app.js @@ -0,0 +1,97 @@ +'use strict'; + +var App = angular.module('app', [ + 'angular-logger', + 'http-auth-interceptor', + 'ngLocale', + 'ngCookies', + 'ngResource', + 'ngSanitize', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.router', + 'ui.utils', + 'angular-growl', + 'angularMoment', + 'spring-security-csrf-token-interceptor', + + //Partials + 'templates', + + //Account + 'app.account.config', 'app.account.directives', 'app.account.controllers', 'app.account.services', + + //Common + 'app.common.config', 'app.common.controllers', 'app.common.directives', 'app.common.services', + + //Todo + 'app.todo.controllers', 'app.todo.directives', 'app.todo.services', + + //Search + 'app.search.controllers', 'app.search.directives', 'app.search.services' + +]); + +App.run(['$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', 'authService', 'AuthenticationService', 'COMMON_EVENTS', + function ($log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser, authService, AuthenticationService, COMMON_EVENTS) { + + var logger = $log.getInstance('app'); + + //This function retries all requests that were failed because of + //the 401 response. + function listenAuthenticationEvents() { + var confirmLogin = function() { + authService.loginConfirmed(); + }; + + $rootScope.$on(AUTH_EVENTS.loginSuccess, confirmLogin); + + var viewLogInPage = function() { + logger.info('User is not authenticated. Rendering login view.'); + $state.go('todo.login'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthenticated, viewLogInPage); + + var viewTodoListPage = function() { + logger.info("User logged out. REndering todo list view."); + $state.go('todo.list', {}, {reload: true}); + }; + + $rootScope.$on(AUTH_EVENTS.logoutSuccess, viewTodoListPage); + + var viewForbiddenPage = function() { + logger.info('Permission was denied for user: %j', AuthenticatedUser); + $state.go('todo.forbidden'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthorized, viewForbiddenPage); + } + + function listenCommonEvents() { + + var view404Page = function() { + logger.info('Requested page was not found.'); + $state.go('todo.404'); + }; + + $rootScope.$on(COMMON_EVENTS.notFound, view404Page); + } + + //This function ensures that anonymous users cannot access states + //that marked as protected (i.e. the value of the authenticated + //property is set to true). + function secureProtectedStates() { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + logger.trace('Moving to state: %s', toState.name); + AuthenticationService.authorizeStateChange(event, toState, toParams); + }); + } + + $rootScope.currentUser = AuthenticatedUser; + + listenAuthenticationEvents(); + listenCommonEvents(); + secureProtectedStates(); +}]); + diff --git a/custom-method-single-repo/frontend/app/assets/i18n/en.json b/custom-method-single-repo/frontend/app/assets/i18n/en.json new file mode 100644 index 0000000..71d4d0c --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/i18n/en.json @@ -0,0 +1,96 @@ +{ + "app.title.label": "Spring Data JPA Tutorial - Query Methods", + "dialogs": { + "delete.dialog": { + "cancel.button.label": "Cancel", + "delete.button.label": "Delete", + "text": "Are you sure that you want to delete the todo entry with title: {{title}}?", + "title": "Delete todo entry?" + } + }, + "directives": { + "login.form": { + "login.button": "Login", + "login.failed": "Login failed!" + }, + "log.out.link.label": "Log Out", + "todo.form": { + "cancel.button": "Cancel", + "save.button": "Save" + } + }, + "footer.message": "Spring Data JPA example application by Petri Kainulainen", + "header.brand.label": "Spring Data JPA Tutorial", + "pages": { + "add.page": { + "title": "Add new todo entry", + "link.label": "Add new todo entry" + }, + "delete.link": "Delete", + "edit.page": { + "link.label": "Edit", + "title": "Edit todo entry" + }, + "forbidden.page": { + "text": "Permission denied.", + "title": "Forbidden" + }, + "not.found.page": { + "text": "The page that you were looking for was not found.", + "title": "Not Found" + }, + "list.page": { + "title": "Things to do", + "texts": { + "no.todo.entries.found": "Nothing to do (yet)." + } + }, + "login.page": { + "title": "Log In" + }, + "search.results.page.title": "Search Results", + "view.page": { + "title": "View Todo Entry" + } + }, + "login": { + "help": "Log in by using username: 'user' and password: 'password'", + "username": "Username", + "username.placeholder": "Enter username", + "password": "Password", + "password.placeholder": "Enter password" + }, + "search": { + "term.field.placeholder": "Search", + "missing.characters.text": "{{missingCharCount}} characters missing" + }, + "todo": { + "created.by.prefix": "by", + "creation.time": "Created at", + "description": "Description", + "description.placeholder": "Enter description", + "messages": { + "description.maxLength": "Description cannot be longer than 500 characters", + "title.maxLength": "Title cannot be longer than 100 characters", + "title.required": "Title is required" + }, + "modified.by.prefix": "by", + "modification.time": "Modified at", + "notifications": { + "add": { + "error": "Adding a new todo entry failed.", + "success": "A new todo entry was added." + }, + "delete": { + "error": "Deleting the todo entry failed.", + "success": "Deleted the todo entry." + }, + "edit": { + "error": "Updating the information of a todo entry failed.", + "success": "Updated the information of the todo entry." + } + }, + "title": "Title", + "title.placeholder": "Enter title" + } +} \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/account/forbidden-view.html b/custom-method-single-repo/frontend/app/assets/partials/account/forbidden-view.html new file mode 100644 index 0000000..c761f3e --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/account/forbidden-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/account/login-form-directive.html b/custom-method-single-repo/frontend/app/assets/partials/account/login-form-directive.html new file mode 100644 index 0000000..d2f14aa --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/account/login-form-directive.html @@ -0,0 +1,36 @@ +
+ + +
+ +
+
+ : + +
+
+ : + +
+
+ +
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/account/login-view.html b/custom-method-single-repo/frontend/app/assets/partials/account/login-view.html new file mode 100644 index 0000000..199d339 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/account/login-view.html @@ -0,0 +1,6 @@ +

+ +
+
+

+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/account/logout-link-directive.html b/custom-method-single-repo/frontend/app/assets/partials/account/logout-link-directive.html new file mode 100644 index 0000000..4d9550a --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/account/logout-link-directive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/common/not-found-view.html b/custom-method-single-repo/frontend/app/assets/partials/common/not-found-view.html new file mode 100644 index 0000000..7edf553 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/common/not-found-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/search/search-form-directive.html b/custom-method-single-repo/frontend/app/assets/partials/search/search-form-directive.html new file mode 100644 index 0000000..674143e --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/search/search-form-directive.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/search/search-result-view.html b/custom-method-single-repo/frontend/app/assets/partials/search/search-result-view.html new file mode 100644 index 0000000..e199e19 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/search/search-result-view.html @@ -0,0 +1,6 @@ +
+

+
+ +
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/add-todo-view.html b/custom-method-single-repo/frontend/app/assets/partials/todo/add-todo-view.html new file mode 100644 index 0000000..0a0406a --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/add-todo-view.html @@ -0,0 +1,9 @@ +

+ +
+
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/delete-todo-modal.html b/custom-method-single-repo/frontend/app/assets/partials/todo/delete-todo-modal.html new file mode 100644 index 0000000..b390319 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/delete-todo-modal.html @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/edit-todo-view.html b/custom-method-single-repo/frontend/app/assets/partials/todo/edit-todo-view.html new file mode 100644 index 0000000..1695ae6 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/edit-todo-view.html @@ -0,0 +1,8 @@ +

+
+
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/todo-form-directive.html b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-form-directive.html new file mode 100644 index 0000000..c7815d0 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-form-directive.html @@ -0,0 +1,52 @@ +
+
+ : + +
+ + +
+
+
+ : + +
+ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-directive.html b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-directive.html new file mode 100644 index 0000000..60ed955 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-directive.html @@ -0,0 +1,8 @@ +
+

+
+ diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-view.html b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-view.html new file mode 100644 index 0000000..6a83ba4 --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/todo-list-view.html @@ -0,0 +1,7 @@ +
+

+ +
+ +
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/assets/partials/todo/view-todo-view.html b/custom-method-single-repo/frontend/app/assets/partials/todo/view-todo-view.html new file mode 100644 index 0000000..374c16d --- /dev/null +++ b/custom-method-single-repo/frontend/app/assets/partials/todo/view-todo-view.html @@ -0,0 +1,25 @@ +
+

+ +
+

{{todoEntry.title}}

+

{{todoEntry.description}}

+
+

+ + {{"todo.creation.time" | translate}}: {{todoEntry.creationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.created.by.prefix" | translate}} {{todoEntry.createdByUser}} + {{"todo.modification.time" | translate }}: {{todoEntry.modificationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.modified.by.prefix" | translate}} {{todoEntry.modifiedByUser}} + +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/account/account.config.js b/custom-method-single-repo/frontend/app/module/account/account.config.js new file mode 100644 index 0000000..c689bb1 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/account/account.config.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('app.account.config', []) + .constant('AUTH_EVENTS', { + loginSuccess: 'event:auth-login-success', + loginFailed: 'event:auth-login-failed', + logoutSuccess: 'event:auth-logout-success', + sessionTimeout: 'event:auth-session-timeout', + notAuthenticated: 'event:auth-loginRequired', + notAuthorized: 'event:auth-forbidden' + }) + .config(['csrfProvider', function(csrfProvider) { + // optional configurations + csrfProvider.config({ + httpTypes: ['PUT', 'POST', 'DELETE'], + maxRetries: 1, + url: '/api/csrf' + }); + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/account/account.controllers.js b/custom-method-single-repo/frontend/app/module/account/account.controllers.js new file mode 100644 index 0000000..79f6fbe --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/account/account.controllers.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('app.account.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.login', { + url: 'login', + controller: 'LoginController', + templateUrl: 'account/login-view.html' + }) + .state('todo.forbidden', { + url: 'forbidden', + controller: 'ForbiddenController', + templateUrl: 'account/forbidden-view.html' + }); + } + ]) + .controller('ForbiddenController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.ForbiddenController'); + logger.info("Rendering forbidden view."); + }]) + .controller('LoginController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.LoginController'); + logger.info('Rendering login form.'); + }]); + diff --git a/custom-method-single-repo/frontend/app/module/account/account.directives.js b/custom-method-single-repo/frontend/app/module/account/account.directives.js new file mode 100644 index 0000000..2ff0aa6 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/account/account.directives.js @@ -0,0 +1,44 @@ +'use strict'; + +angular.module('app.account.directives', []) + .directive('logOutLink', ['$log', 'AuthenticationService', function ($log, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.logOutLink'); + + return { + link: function (scope, element, attr) { + scope.logOut = function() { + logger.info('Logging user out.'); + AuthenticationService.logOut(); + }; + }, + templateUrl: 'account/logout-link-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('loginForm', ['$log', 'AUTH_EVENTS', 'AuthenticationService', function ($log, AUTH_EVENTS, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.loginForm'); + + return { + link: function (scope, element, attr) { + scope.login = {}; + scope.loginFailed = false; + + scope.$on(AUTH_EVENTS.loginFailed, function() { + logger.info('Received login failed event.'); + scope.loginFailed = true; + }); + + scope.submitLoginForm = function() { + logger.info('Submitting log in form.'); + AuthenticationService.logIn(scope.login.username, scope.login.password); + }; + }, + templateUrl: 'account/login-form-directive.html', + scope: { + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/account/account.services.js b/custom-method-single-repo/frontend/app/module/account/account.services.js new file mode 100644 index 0000000..7cbd82d --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/account/account.services.js @@ -0,0 +1,79 @@ +'use strict'; + +angular.module('app.account.services', ['ngResource']) + .service('AuthenticatedUser', function () { + this.create = function (username, role) { + this.username = username; + this.role = role; + }; + this.destroy = function () { + this.username = null; + this.role = null; + }; + }) + .factory('AuthenticationService', ['$http', '$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', + function($http, $log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser) { + + var logger = $log.getInstance('app.account.services.AuthenticationService'); + + return { + authorizeStateChange: function(event, toState, toParams) { + logger.debug('Authorizing state change to state: %s', toState.name); + if (toState.authenticate && !this.isAuthenticated()) { + event.preventDefault(); + + logger.debug('Authentication is not found. Fetching it from the backend.'); + var self = this; + $http.get('/api/authenticated-user').success(function(user) { + logger.debug('Found authenticated user: %j', user); + AuthenticatedUser.create(user.username, user.role); + + if (!self.isAuthenticated) { + logger.debug('Unauthenticated users is: %j', AuthenticatedUser); + $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); + } + else { + logger.debug('User is authenticated. Continuing to the target state: %s', toState.name); + $state.go(toState.name, toParams); + } + }); + } + }, + isAuthenticated: function() { + logger.debug('Checking if user: %j is authenticated.', AuthenticatedUser); + return AuthenticatedUser.username; + }, + logIn: function(username, password) { + logger.info('Logging in user with username: %s', username); + + var transform = function(data){ + return $.param(data); + }; + + $http.post('/api/login', {username: username, password: password}, { + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + ignoreAuthModule: true, + transformRequest: transform + }) + .success(function(user) { + logger.info('Login successful for user: %j', user); + AuthenticatedUser.create(user.username, user.role); + $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); + }) + .error(function() { + logger.info('Login failed'); + $rootScope.$broadcast(AUTH_EVENTS.loginFailed); + }); + }, + logOut: function() { + if (this.isAuthenticated()) { + $http.post('/api/logout', {}) + .success(function() { + logger.info('User is logged out.'); + AuthenticatedUser.destroy(); + $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); + }); + } + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/common/common.config.js b/custom-method-single-repo/frontend/app/module/common/common.config.js new file mode 100644 index 0000000..6ffca02 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/common/common.config.js @@ -0,0 +1,60 @@ +'use strict'; + +angular.module('app.common.config', []) + .constant('COMMON_EVENTS', { + notFound: 'event:not-found' + }) + .config(['logEnhancerProvider', function (logEnhancerProvider) { + logEnhancerProvider.datetimePattern = 'DD.MM.YYYY HH:mm:ss'; + logEnhancerProvider.prefixPattern = '%s::[%s]> '; + logEnhancerProvider.logLevels = { + '*': logEnhancerProvider.LEVEL.OFF + }; + }]) + .config(['$urlRouterProvider', '$locationProvider', + function ($urlRouterProvider, $locationProvider) { + //this prevents infinite $digest loop when we invoke the + //preventDefault() method in $stateChangeStart event handler. + //See: https://github.com/angular-ui/ui-router/issues/600#issuecomment-47228922 + $urlRouterProvider.otherwise( function($injector, $location) { + var $state = $injector.get("$state"); + $state.go("todo.list"); + }); + + // Without server side support html5 must be disabled. + $locationProvider.html5Mode(false); + } + ]) + .config(['$translateProvider', function ($translateProvider) { + // Initialize angular-translate + $translateProvider.useStaticFilesLoader({ + prefix: '/i18n/', + suffix: '.json' + }); + + $translateProvider.preferredLanguage('en'); + $translateProvider.useSanitizeValueStrategy('escaped'); + $translateProvider.useLocalStorage(); + $translateProvider.useMissingTranslationHandlerLog(); + }]) + .config(['growlProvider', function (growlProvider) { + growlProvider.globalTimeToLive(5000); + }]) + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push([ + '$injector', + function ($injector) { + return $injector.get('404Interceptor'); + } + ]); + }]) + .factory('404Interceptor', ['$rootScope', '$q', 'COMMON_EVENTS', function ($rootScope, $q, COMMON_EVENTS) { + return { + responseError: function(response) { + if (response.status === 404) { + $rootScope.$broadcast(COMMON_EVENTS.notFound); + } + return $q.reject(response); + } + }; + }]); diff --git a/custom-method-single-repo/frontend/app/module/common/common.controllers.js b/custom-method-single-repo/frontend/app/module/common/common.controllers.js new file mode 100644 index 0000000..811f15e --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/common/common.controllers.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('app.common.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.404', { + url: 'not-found', + controller: 'NotFoundController', + templateUrl: 'common/not-found-view.html' + }); + } + ]) + .controller('NotFoundController', ['$log', function($log) { + var logger = $log.getInstance('app.common.controllers.NotFoundController'); + logger.info("Rendering 404 view."); + }]); + diff --git a/custom-method-single-repo/frontend/app/module/common/common.directives.js b/custom-method-single-repo/frontend/app/module/common/common.directives.js new file mode 100644 index 0000000..7c56027 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/common/common.directives.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('app.common.directives', []) + .directive('staticInclude', ['$http', '$templateCache', '$compile', function ($http, $templateCache, $compile) { + return function(scope, element, attrs) { + var templatePath = attrs.staticInclude; + + $http.get(templatePath, {cache: $templateCache}).success(function (response) { + var contents = $('
').html(response).contents(); + element.html(contents); + $compile(contents)(scope); + }); + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/common/common.services.js b/custom-method-single-repo/frontend/app/module/common/common.services.js new file mode 100644 index 0000000..de9d0e6 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/common/common.services.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('app.common.services', []) + .service('NotificationService', ['$rootScope', 'growl', function ($rootScope, growl) { + var flashMessageQueue = []; + + function displayNotification(message, type) { + if (type === 'success') { + growl.success(message); + } else if (type === 'warn') { + growl.warning(message); + } else if (type === 'info') { + growl.info(message); + } else { + growl.error(message); + } + } + + // Display all flash notifications after state has changed + $rootScope.$on("$stateChangeSuccess", function () { + while (flashMessageQueue.length > 0) { + var item = flashMessageQueue.shift(); + if (item) { + displayNotification(item.message, item.type); + } + } + }); + + // Public API + return { + 'flashMessage': function (message, type) { + flashMessageQueue.push({message: message, type: type || 'info'}); + } + }; + }]); diff --git a/custom-method-single-repo/frontend/app/module/search/search.controllers.js b/custom-method-single-repo/frontend/app/module/search/search.controllers.js new file mode 100644 index 0000000..f400438 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/search/search.controllers.js @@ -0,0 +1,30 @@ +'use strict'; + +angular.module('app.search.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.search', { + authenticate: true, + url: 'todo/search/:searchTerm', + controller: 'SearchResultController', + templateUrl: 'search/search-result-view.html', + resolve: { + searchResults: ['TodoSearchService', '$stateParams', function(TodoSearchService, $stateParams) { + if ($stateParams.searchTerm) { + return TodoSearchService.findBySearchTerm($stateParams.searchTerm); + } + + return null; + }] + } + }); + } + ]) + .controller('SearchResultController', ['$log', '$scope', 'searchResults', + function($log, $scope, searchResults) { + var logger = $log.getInstance('app.search.controllers.SearchResultController'); + logger.info('Rendering search results page.'); + $scope.todoEntries = searchResults; + }]); + diff --git a/custom-method-single-repo/frontend/app/module/search/search.directives.js b/custom-method-single-repo/frontend/app/module/search/search.directives.js new file mode 100644 index 0000000..fa7be29 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/search/search.directives.js @@ -0,0 +1,66 @@ +'use strict'; + +angular.module('app.search.directives', []) + .directive('searchForm', ['$log', '$state', function($log, $state) { + + var logger = $log.getInstance('app.search.directives.searchForm'); + + return { + link: function (scope, element, attr) { + var userWritingSearchTerm = false; + var minimumSearchTermLength = 3; + + scope.translationData = { + missingCharCount: minimumSearchTermLength + }; + + scope.search = {}; + scope.search.searchTerm = ""; + + scope.searchFieldBlur = function() { + userWritingSearchTerm = false; + scope.search.searchTerm = ""; + scope.translationData.missingCharCount = minimumSearchTermLength; + }; + + scope.searchFieldFocus = function() { + userWritingSearchTerm = true; + }; + + scope.showMissingCharacterText = function() { + if (!scope.search.searchTerm) { + scope.search.searchTerm = ""; + } + + if (userWritingSearchTerm) { + if (scope.search.searchTerm.length < minimumSearchTermLength) { + return true; + } + } + + return false; + }; + + scope.search = function() { + logger.trace('User is using the search term: %s', scope.search.searchTerm); + + if (scope.search.searchTerm.length < minimumSearchTermLength) { + scope.translationData.missingCharCount = minimumSearchTermLength - scope.search.searchTerm.length; + logger.trace('%s characters are missing. Search is not invoked.', scope.translationData.missingCharCount); + } + else { + scope.translationData.missingCharCount = 0; + $state.go('todo.search', + {searchTerm: scope.search.searchTerm}, + {reload: true, inherit: true, notify: true} + ); + } + }; + + }, + templateUrl: 'search/search-form-directive.html', + scope: { + currentUser: '=' + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/search/search.services.js b/custom-method-single-repo/frontend/app/module/search/search.services.js new file mode 100644 index 0000000..2c18447 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/search/search.services.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('app.search.services', ['ngResource']) + .factory('TodoSearchService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/search', {}, { + 'query': {method:'GET', isArray:true} + }); + + var logger = $log.getInstance('app.search.services.TodoSearchService'); + + return { + findBySearchTerm: function(searchTerm) { + logger.info('Searching todo entries with search term: %s', searchTerm); + return api.query({searchTerm: searchTerm}); + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/todo/todo.controllers.js b/custom-method-single-repo/frontend/app/module/todo/todo.controllers.js new file mode 100644 index 0000000..d4da7bf --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/todo/todo.controllers.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('app.todo.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo', { + url: '/', + abstract: true, + template: '' + }) + .state('todo.add', { + authenticate: true, + url: 'todo/add', + controller: 'AddTodoController', + templateUrl: 'todo/add-todo-view.html' + }) + .state('todo.edit', { + authenticate: true, + url: 'todo/:id/edit', + controller: 'EditTodoController', + templateUrl: 'todo/edit-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }) + .state('todo.list', { + authenticate: true, + url: '', + controller: 'TodoListController', + templateUrl: 'todo/todo-list-view.html', + resolve: { + todoEntries: ['TodoService', function(TodoService) { + return TodoService.findAll(); + }] + } + }) + .state('todo.view', { + authenticate: true, + url: 'todo/:id', + controller: 'ViewTodoController', + templateUrl: 'todo/view-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }); + } + ]) + .controller('AddTodoController', ['$log', '$scope', function($log, $scope) { + var logger = $log.getInstance('app.todo.controllers.AddTodoController'); + logger.info('Rendering add todo entry page.'); + $scope.todoEntry = {}; + }]) + .controller('EditTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.EditTodoController'); + logger.info('Rendering edit todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]) + .controller('TodoListController', ['$log', '$scope', 'todoEntries', function($log, $scope, todoEntries) { + var logger = $log.getInstance('app.todo.controllers.TodoListController'); + logger.info('Rendering todo entry list page for %s todo entries.', todoEntries.length); + $scope.todoEntries = todoEntries; + }]) + .controller('ViewTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.ViewTodoController'); + logger.info('Rendering view todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/todo/todo.directives.js b/custom-method-single-repo/frontend/app/module/todo/todo.directives.js new file mode 100644 index 0000000..a2377b5 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/todo/todo.directives.js @@ -0,0 +1,102 @@ +'use strict'; + +angular.module('app.todo.directives', []) + .controller('DeleteTodoController', ['$log', '$scope', '$modalInstance', '$state', 'TodoService', 'todoEntry', 'successCallback', 'errorCallback', + function($log, $scope, $modalInstance, $state, TodoService, todoEntry, successCallback, errorCallback) { + var logger = $log.getInstance('app.todo.directives.DeleteTodoController'); + + logger.info('Showing delete confirmation dialog for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + + $scope.cancel = function() { + logger.info('User clicked cancel button. Todo entry is not deleted.'); + $modalInstance.dismiss('cancel'); + }; + + $scope.delete = function() { + logger.info('User clicked delete button. Todo entry is deleted.'); + $modalInstance.close(); + TodoService.delete(todoEntry, successCallback, errorCallback); + }; + }]) + .directive('deleteTodoEntryButton', ['$modal', '$state', 'NotificationService', function($modal, $state, NotificationService) { + return { + link: function (scope, element, attr) { + scope.onSuccess = function() { + NotificationService.flashMessage('todo.notifications.delete.success', 'success'); + $state.go('todo.list'); + }; + + scope.onError = function() { + NotificationService.flashMessage('todo.notifications.delete.error', 'error'); + }; + + scope.showDeleteConfirmationDialog = function() { + $modal.open({ + templateUrl: 'todo/delete-todo-modal.html', + controller: 'DeleteTodoController', + resolve: { + errorCallback: function() { + return scope.onError; + }, + successCallback: function() { + return scope.onSuccess; + }, + todoEntry: function () { + return scope.todoEntry; + } + } + }); + }; + }, + template: '', + scope: { + todoEntry: '=' + } + }; + }]) + .directive('todoEntryForm', ['$log', '$state', 'NotificationService', 'TodoService', function($log, $state, NotificationService, TodoService) { + var logger = $log.getInstance('app.todo.directives.todoEntryForm'); + + return { + link: function (scope, element, attr) { + scope.saveTodoEntry = function() { + logger.info('Saving todo entry: %j', scope.todoEntry); + + var onSuccess = function(saved) { + NotificationService.flashMessage(scope.successMessageKey, 'success'); + $state.go('todo.view', {id: saved.id}); + }; + + var onError = function() { + NotificationService.flashMessage(scope.errorMessageKey, 'errors'); + }; + + if (scope.formType === 'add') { + TodoService.add(scope.todoEntry, onSuccess, onError); + } + else if (scope.formType === 'edit') { + TodoService.update(scope.todoEntry, onSuccess, onError); + } + else { + logger.error('Unknown form type: %s', scope.formType); + } + }; + }, + templateUrl: 'todo/todo-form-directive.html', + scope: { + errorMessageKey: '@', + formType: '@', + todoEntry: '=', + successMessageKey: '@' + } + }; + }]) + .directive('todoEntryList', [function() { + return { + templateUrl: 'todo/todo-list-directive.html', + scope: { + todoEntries: '=' + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/module/todo/todo.services.js b/custom-method-single-repo/frontend/app/module/todo/todo.services.js new file mode 100644 index 0000000..f49f622 --- /dev/null +++ b/custom-method-single-repo/frontend/app/module/todo/todo.services.js @@ -0,0 +1,61 @@ +'use strict'; + +angular.module('app.todo.services', ['ngResource']) + .factory('TodoService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/:id', {"id": "@id"}, { + get: {method: 'GET'}, + save: {method: 'POST'}, + update: {method: 'PUT'}, + query: {method: 'GET', params: {}, isArray: true} + }); + + var logger = $log.getInstance('app.todo.services.TodoService'); + + return { + add: function(todo, successCallback, errorCallback) { + logger.info('Adding new todo entry: %j', todo); + return api.save(todo, + function(added) { + logger.info('Added a new todo entry: %j', added); + successCallback(added); + }, + function(error) { + logger.error('Adding a todo entry failed because of an error: %j', error); + errorCallback(error); + }); + }, + delete: function(todo, successCallback, errorCallback) { + logger.info('Deleting todo entry: %j', todo); + return api.delete(todo, + function(deleted) { + logger.info('Deleted todo entry: %j', deleted); + successCallback(deleted); + }, + function(error) { + logger.error('Deleting the todo entry failed because of an error: %j', error); + errorCallback(error); + } + ); + }, + findAll: function() { + logger.info('Finding all todo entries.'); + return api.query(); + }, + findById: function(id) { + logger.info('Finding todo entry by id: %s', id); + return api.get({id: id}).$promise; + }, + update: function(todo, successCallback, errorCallback) { + logger.info('Updating todo entry: %j', todo); + return api.update(todo, + function(updated) { + logger.info('Updated the information of the todo entry: %j', updated); + successCallback(updated); + }, + function(error) { + logger.error('Updating the information of the todo entry failed because of an error: %j', error); + errorCallback(error); + }); + } + }; + }]); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/app/styles/app.less b/custom-method-single-repo/frontend/app/styles/app.less new file mode 100644 index 0000000..4e70998 --- /dev/null +++ b/custom-method-single-repo/frontend/app/styles/app.less @@ -0,0 +1,74 @@ +[ng-cloak] { + display: none; +} + +@import "https://github.com/bower_components/bootstrap/less/bootstrap.less"; + +// Red asterisk for required labels +label.required:before{ + content:"* "; + color:red; +} + +// styles for custom input validation +input.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +input.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +textarea.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +small.ng-error { + color: #a94442; +} + +a:hover { + cursor: pointer; +} + +.striped-list { + > .row:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + +.striped-list .row { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; +} + +.action-buttons { + text-align: right; +} diff --git a/custom-method-single-repo/frontend/bower.json b/custom-method-single-repo/frontend/bower.json new file mode 100644 index 0000000..c19dab2 --- /dev/null +++ b/custom-method-single-repo/frontend/bower.json @@ -0,0 +1,39 @@ +{ + "name": "Spring Data JPA Tutorial - Query Methods", + "version": "0.0.1", + "main": "_public/frontend/js/app.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "console-polyfill": "~0.2.1", + "lodash": "~3.8.0", + "moment": "2.10.6", + "jquery": "2.1.0", + "bootstrap": "~3.3.4", + "angular": "~1.3.15", + "angular-http-auth": "1.2.2", + "angular-i18n": "~1.3.15", + "angular-moment": "0.10.1", + "angular-logger": "1.0.1", + "angular-sanitize": "~1.3.15", + "angular-resource": "~1.3.15", + "angular-cookies": "~1.3.15", + "angular-loader": "~1.3.15", + "angular-mocks": "~1.3.15", + "angular-translate": "~2.7.0", + "angular-translate-storage-local": "~2.7.0", + "angular-translate-loader-static-files": "~2.7.0", + "angular-translate-handler-log": "~2.7.0", + "angular-ui-utils": "~0.2.3", + "angular-ui-router": "~0.2.15", + "angular-bootstrap": "~0.13.0", + "angular-growl-v2": "0.7.3", + "es5-shim": "~4.1.1", + "json3": "~3.3.2", + "script.js": "~2.5.7", + "sprintf": "1.0.3" + } +} diff --git a/custom-method-single-repo/frontend/build.config.js b/custom-method-single-repo/frontend/build.config.js new file mode 100644 index 0000000..1ec8575 --- /dev/null +++ b/custom-method-single-repo/frontend/build.config.js @@ -0,0 +1,76 @@ +'use strict'; + +var path = require('path'); + +var targetBase = './build/'; + +module.exports = { + //Configures the directories in which the files created by Gulp are copied. + target: { + js: targetBase + '/js', + lib: path.join(targetBase, 'js', 'lib'), + css: path.join(targetBase, 'css'), + partials: path.join(targetBase, 'partials'), + assets: targetBase + }, + + //Configures the location of the used libraries and frameworks. + vendorFiles: { + code: [ + './bower_components/console-polyfill/index.js', + './bower_components/lodash/dist/lodash.min.js', + './bower_components/jquery/dist/jquery.min.js', + './bower_components/angular/angular.js', + './bower_components/moment/min/moment-with-locales.min.js', + './bower_components/sprintf/dist/sprintf.min.js', + './bower_components/angular-http-auth/src/http-auth-interceptor.js', + './bower_components/angular-i18n/angular-locale_fi-fi.js', + './bower_components/angular-cookies/angular-cookies.min.js', + './bower_components/angular-moment/angular-moment.min.js', + './bower_components/angular-logger/dist/angular-logger.min.js', + './bower_components/angular-resource/angular-resource.min.js', + './bower_components/angular-sanitize/angular-sanitize.min.js', + './bower_components/angular-translate/angular-translate.min.js', + './bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', + './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js', + './bower_components/angular-translate-storage-local/angular-translate-storage-local.min.js', + './bower_components/angular-translate-handler-log/angular-translate-handler-log.min.js', + './bower_components/angular-ui-router/release/angular-ui-router.min.js', + './bower_components/angular-ui-utils/ui-utils.min.js', + './bower_components/angular-ui-utils/ui-utils-ieshiv.min.js', + './bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', + './bower_components/angular-growl-v2/build/angular-growl.min.js', + './vendor/spring-security-csrf-token-interceptor/src/spring-security-csrf-token-interceptor.js' + ] + }, + + //Configures the location of our application's files. + appFiles: { + //Configures the location of the Javascript files. + code: [ + "./app/**/*.js" + ], + //Configures the location of the LESS files. + styleBase: "./app/styles/", + style: [ + "./bower_components/angular-growl-v2/build/angular-growl.min.css", + "./app/styles/app.less" + ], + //Configures the location of the view templates. + partials: [ + "./app/assets/partials/**/*.html" + ], + //Configures the location of static assets such as images, fonts, and localization files. + assetsBase: './app/assets/', + assets: [ + './app/assets/**' + ], + //Configures the location of shims (libraries that bring new APIs to older browsers) + shim: [ + './bower_components/angular-loader/angular-loader.min.js', + './bower_components/script.js/dist/script.min.js', + './bower_components/es5-shim/es5-shim.min.js', + './bower_components/json3/lib/json3.min.js' + ] + } +}; diff --git a/custom-method-single-repo/frontend/gulpfile.js b/custom-method-single-repo/frontend/gulpfile.js new file mode 100644 index 0000000..40f10f7 --- /dev/null +++ b/custom-method-single-repo/frontend/gulpfile.js @@ -0,0 +1,124 @@ +var gulp = require("gulp"); +var plugins = require('gulp-load-plugins')(); +var config = require('./build.config.js'); + +//Analyzes the Javascript files of our application by using JSHint and reports the found problems. +gulp.task('jshint', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.changed(config.target.js)) + .pipe(plugins.jshint('.jshintrc')) + .pipe(plugins.jshint.reporter('jshint-stylish')); +}); + +//Processes the Javascript files of our application. +gulp.task('appCode', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.sourcemaps.init()) + //Combines the Javascript files into a single Javascript file + .pipe(plugins.concat('app.min.js')) + //Minifies the created Javascript file + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.sourcemaps.write()) + //Copies the minified Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file. + .pipe(plugins.size({title: 'application'})) +}); + +//Processes the HTML templates of our application. +gulp.task('appPartials', function () { + return gulp.src(config.appFiles.partials) + .pipe(plugins.changed(config.target.js)) + //Minifies the HTML files + .pipe(plugins.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + //Loads the HTML templates into AngularJS $templateCache + .pipe(plugins.angularTemplatecache('partials.js', { + standalone: true + })) + //Copy the created Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of created Javascript file + .pipe(plugins.size({showFiles: true})) +}); + +//Processes the LESS files of our application. +gulp.task('appLess', function () { + return gulp.src(config.appFiles.style) + //Creates the final CSS file + .pipe(plugins.less({ + paths: [config.appFiles.styleBase] + })) + .pipe(plugins.concat('app.css')) + //Minifies the created CSS file + .pipe(plugins.minifyCss()) + //Copies the CSS File into the target directory + .pipe(gulp.dest(config.target.css)) + //Reports the size of the final CSS file. + .pipe(plugins.size({ title: 'css' })) +}); + +gulp.task('appAssets', function () { + return gulp.src(config.appFiles.assets, {base: config.appFiles.assetsBase}) + .pipe(gulp.dest(config.target.assets)) +}); + +//Minimizes the shims used by our application and copies them to the target directory. +gulp.task('appShim', function () { + return gulp.src(config.appFiles.shim) + .pipe(plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + })) + .pipe(gulp.dest(config.target.lib)); +}); + +//Processes the Javascript files of the libraries and frameworks that are used in our application +gulp.task('vendorCode', function () { + return gulp.src(config.vendorFiles.code) + //Combine the Javascript files into a single Javascript file + .pipe(plugins.concat('vendor.min.js')) + //Skips minification of files that are already minified. + .pipe(plugins.if('*.min.js', plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + }))) + //Minifies Javascript files that are not minified. + .pipe(plugins.if('vendor/**/*.js', plugins.uglify({ + mangle: false, + compress: true + }))) + //Copies the created file to the target directory. + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file + .pipe(plugins.size({title: 'vendor'})) +}); + +//Analyzes our Javascript files by using JSHint and invokes the build when the watched files are changed +gulp.task('watch', ['jshint', 'build'], function () { + gulp.watch(config.appFiles.partials, ['appPartials']); + gulp.watch(config.appFiles.code, ['appCode', 'jshint']); + gulp.watch(config.appFiles.style, ['appLess']); + gulp.watch(config.appFiles.assets, ['appAssets']); + gulp.watch(config.vendorFiles.code, ['vendorCode']); +}); + +//Configures the tasks of our build +gulp.task('build', [ + 'appLess', + 'appShim', + 'appAssets', + 'appPartials', + 'appCode', + 'vendorCode' +]); + +//Runs the watch task if no task is specified when gulp is run +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/custom-method-single-repo/frontend/package.json b/custom-method-single-repo/frontend/package.json new file mode 100644 index 0000000..55dfccd --- /dev/null +++ b/custom-method-single-repo/frontend/package.json @@ -0,0 +1,38 @@ +{ + "author": "Petri Kainulainen", + "name": "spring-data-jpa-tutorial-query-methods", + "description": "Angular frontend for a Spring Data JPA example.", + "version": "1.0.0", + "homepage": "", + "repository": { + "type": "git", + "url": "" + }, + "dependencies": { + "bower": "~1.4.1", + "gulp": "~3.8.11", + "gulp-angular-templatecache": "~1.6.0", + "gulp-changed": "~1.2.1", + "gulp-concat": "~2.5.2", + "gulp-if": "~1.2.5", + "gulp-insert": "^0.4.0", + "gulp-jshint": "~1.10.0", + "gulp-less": "~3.0.3", + "gulp-load-plugins": "~0.10.0", + "gulp-minify-css": "~1.1.1", + "gulp-minify-html": "~1.0.2", + "gulp-rename": "~1.2.2", + "gulp-size": "~1.2.1", + "gulp-sourcemaps": "~1.5.2", + "gulp-uglify": "~1.2.0", + "jshint-stylish": "~1.0.2" + }, + "engines": { + "node": ">=0.12.0" + } +} + + + + + diff --git a/custom-method-single-repo/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js b/custom-method-single-repo/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js new file mode 100644 index 0000000..84318da --- /dev/null +++ b/custom-method-single-repo/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("spring-security-csrf-token-interceptor",[]).factory("csrfInterceptor",["$injector","$q",function($injector){var $q=$injector.get("$q"),csrf=$injector.get("csrf"),csrfService=csrf.init();return{request:function(config){return csrfService.settings.httpTypes.indexOf(config.method.toUpperCase())>-1&&(config.headers[csrfService.settings.csrfTokenHeader]=csrfService.token),config||$q.when(config)},responseError:function(response){var $http,newToken=response.headers(csrfService.settings.csrfTokenHeader);return 403===response.status&&csrfService.numRetries -1) { + config.headers[csrfService.settings.csrfTokenHeader] = csrfService.token; + } + return config || $q.when(config); + }, + responseError: function(response) { + var $http, + newToken = response.headers(csrfService.settings.csrfTokenHeader); + + if (response.status === 403 && csrfService.numRetries < csrfService.settings.maxRetries) { + csrfService.getTokenData(); + $http = $injector.get('$http'); + csrfService.numRetries = csrfService.numRetries + 1; + return $http(response.config); + } else if (newToken) { + // update the csrf token in-case of response errors other than 403 + csrfService.token = newToken; + } + // Fix for interceptor causing failing requests + return $q.reject(response); + }, + response: function(response) { + // reset number of retries on a successful response + csrfService.numRetries = 0; + return response; + } + }; + } + ]).factory('csrfService', [ + + function() { + var defaults = { + url: '/', // the URL to which the CSRF call has to be made to get the token + csrfHttpType: 'head', // the HTTP method type which is used for making the CSRF token call + maxRetries: 5, // number of retires allowed for forbidden requests + csrfTokenHeader: 'X-CSRF-TOKEN', + httpTypes: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] // default allowed HTTP types + }; + return { + inited: false, + settings: null, + numRetries: 0, + token: '', + init: function(options) { + this.settings = angular.extend({}, defaults, options); + this.getTokenData(); + console.log(this.settings, this.defaults, options); + }, + getTokenData: function() { + var xhr = new XMLHttpRequest(); + xhr.open(this.settings.csrfHttpType, this.settings.url, false); + xhr.send(); + + this.token = xhr.getResponseHeader(this.settings.csrfTokenHeader); + this.inited = true; + } + }; + + } + ]).provider('csrf', [ + + function() { + var CsrfModel = function CsrfModel(options) { + return { + options: options, + csrfService: null + }; + }; + + return { + $get: ['csrfService', + function(csrfService) { + var self = this; + return { + init: function() { + self.model = new CsrfModel(self.options); + self.model.csrfService = csrfService; + self.model.csrfService.init(self.model.options); + return self.model.csrfService; + } + }; + } + ], + + model: null, + + options: {}, + + config: function(options) { + this.options = options; + } + }; + } + ]).config(['$httpProvider', + function($httpProvider) { + $httpProvider.interceptors.push('csrfInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/custom-method-single-repo/pom.xml b/custom-method-single-repo/pom.xml new file mode 100644 index 0000000..a85d866 --- /dev/null +++ b/custom-method-single-repo/pom.xml @@ -0,0 +1,375 @@ + + 4.0.0 + net.petrikainulainen.springdata.jpa + custom-methods-single-repo + 0.1 + Spring Data JPA - Adding Custom Methods Into a Single Repository + war + + This example demonstrates how you can add custom methods into a single + Spring Data JPA repository. + + + + + + io.spring.platform + platform-bom + 1.1.2.RELEASE + pom + import + + + + + + 1.8 + UTF-8 + true + false + 4.0.1.RELEASE + + + + + dev + + + integration-test + + false + true + + + + + + + + org.apache.commons + commons-lang3 + + + + org.slf4j + slf4j-api + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + + com.h2database + h2 + + + + com.zaxxer + HikariCP + + + + org.hibernate + hibernate-entitymanager + + + + org.jadira.usertype + usertype.extended + 3.2.0.GA + + + + org.springframework.data + spring-data-jpa + + + + org.springframework + spring-aspects + + + org.springframework + spring-context-support + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet + jstl + + + org.springframework + spring-webmvc + + + org.hibernate + hibernate-validator + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + + + javax.el + javax.el-api + test + + + org.glassfish.web + el-impl + 2.2 + test + + + junit + junit + test + + + com.nitorcreations + junit-runners + 1.3 + test + + + org.assertj + assertj-core + 3.1.0 + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + info.solidsoft.mockito + mockito-java8 + 0.3.0 + test + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.security.version} + test + + + com.jayway.jsonpath + json-path + test + + + com.jayway.jsonpath + json-path-assert + 0.9.1 + test + + + com.github.springtestdbunit + spring-test-dbunit + 1.2.1 + test + + + org.dbunit + dbunit + 2.5.1 + test + + + junit + junit + + + + + + ROOT + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-integration-test-sources + generate-test-sources + + add-test-source + + + + src/integration-test/java + + + + + add-integration-test-resources + generate-test-resources + + add-test-resource + + + + + src/integration-test/resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${jdk.version} + ${jdk.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + ROOT + false + + + frontend/build + / + false + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + ${skip.unit.tests} + + **/IT*.java + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18 + + + + integration-tests + + integration-test + verify + + + + ${skip.integration.tests} + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.2.10.v20150310 + + 0 + stop + 9999 + + + spring.profiles.active + application + + + + ${project.basedir}/target/ROOT.war + / + + ${project.basedir}/src/main/webapp + ${project.basedir}/frontend/build + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + + run + + + + + + + diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java new file mode 100644 index 0000000..471aeae --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * This class contains the constants that are used in our integration tests, DbUnit datasets, + * and the localization file. + * + * @author Petri Kainulainen + */ +public final class TodoConstants { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "description"; + public static final Long ID = 1L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "title"; + + public static final String SEARCH_TERM_DESCRIPTION_MATCHES = "esC"; + public static final String SEARCH_TERM_NO_MATCH = "NO MATCH"; + public static final String SEARCH_TERM_TITLE_MATCHES = "It"; + + public static final String UPDATED_DESCRIPTION = "updatedDescription"; + public static final String UPDATED_TITLE = "updatedTitle"; + + public static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; + public static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; + public static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; + public static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; + + /** + * Prevents instantiation + */ + private TodoConstants() {} +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java new file mode 100644 index 0000000..77cdb31 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * @author Petri Kainulainen + */ +public enum Users { + + USER("user", "password", "ROLE_USER"); + + private String password; + private String role; + private String username; + + Users(String username, String password, String role) { + this.password = password; + this.role = role; + this.username = username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } + + public String getUsername() { + return username; + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java new file mode 100644 index 0000000..7ab64da --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java @@ -0,0 +1,80 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITTodoRepositoryTest { + + private static final String SEARCH_TERM = "tIo"; + private static final Long SECOND_TODO_ID = 2L; + + @Autowired + private TodoRepository repository; + + @Test + public void findBySearchTerm_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + TodoSearchResultDTO todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.ID); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTerm_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + TodoSearchResultDTO todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.ID); + } + + @Test + public void findBySearchTerm_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTerm(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + TodoSearchResultDTO first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(SECOND_TODO_ID); + + TodoSearchResultDTO second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.ID); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java new file mode 100644 index 0000000..af912d1 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java @@ -0,0 +1,27 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.dataset.FlatXmlDataSetLoader; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ReplacementDataSet; +import org.springframework.core.io.Resource; +/** + * This class is a custom DbUnit data set loader that support flat XML data sets. This data set loader + * adds support for the extra features: + *
    + *
  • You can use the column sensing feature of DbUnit.
  • + *
  • You can specify that a column's value is null by using the string [null].
  • + *
+ * @author Petri Kainulainen + */ +public class ColumnSensingReplacementDataSetLoader extends FlatXmlDataSetLoader { + + @Override + protected IDataSet createDataSet(Resource resource) throws Exception { + return createReplacementDataSet(super.createDataSet(resource)); + } + private ReplacementDataSet createReplacementDataSet(IDataSet dataSet) { + ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet); + replacementDataSet.addReplacementObject("[null]", null); + return replacementDataSet; + } +} \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java new file mode 100644 index 0000000..4360756 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Petri Kainulainen + */ +public final class DbTestUtil { + + private DbTestUtil() {} + + public static void resetAutoIncrementColumns(ApplicationContext applicationContext, + String... tableNames) throws SQLException { + DataSource dataSource = applicationContext.getBean(DataSource.class); + String resetSqlTemplate = getResetSqlTemplate(applicationContext); + try (Connection dbConnection = dataSource.getConnection()) { + //Create SQL statements that reset the auto increment columns and invoke + //the created SQL statements. + for (String resetSqlArgument: tableNames) { + try (Statement statement = dbConnection.createStatement()) { + String resetSql = String.format(resetSqlTemplate, resetSqlArgument); + statement.execute(resetSql); + } + } + } + } + + private static String getResetSqlTemplate(ApplicationContext applicationContext) { + //Read the SQL template from the properties file + Environment environment = applicationContext.getBean(Environment.class); + return environment.getRequiredProperty("test.reset.sql.template"); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java new file mode 100644 index 0000000..d3f6fe1 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java @@ -0,0 +1,251 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +@DatabaseSetup("no-todo-entries.xml") +public class ITCreateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + DbTestUtil.resetAutoIncrementColumns(webAppContext, "todos"); + + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void create_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isCreated()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.creationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } + + @Test + @ExpectedDatabase(value = "create-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.DESCRIPTION) + .title(TodoConstants.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java new file mode 100644 index 0000000..341b271 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITDeleteTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void delete_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("delete-todo-entry-expected.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID) + .with(csrf()) + ); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java new file mode 100644 index 0000000..c1d648c --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java @@ -0,0 +1,97 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindAllTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findAll_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findAll_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenTodoEntriesAreNotFound_ShouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenOneTodoEntryIsFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java new file mode 100644 index 0000000..eac4311 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java @@ -0,0 +1,107 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindByIdTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void findById_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java new file mode 100644 index 0000000..6ccc104 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java @@ -0,0 +1,181 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("two-todo-entries.xml") +public class ITFindBySearchTermTest { + + private static final Long SECOND_TODO_ID = 2L; + private static final String SECOND_TODO_TITLE = "First"; + + private static final String SEARCH_TERM = "tIo"; + + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findBySearchTerm_AsAnonymous_ShouldReturnHttpResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnZeroTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTerm_ShouldTwoTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].id", is(SECOND_TODO_ID.intValue()))) + .andExpect(jsonPath("$[0].title", is(SECOND_TODO_TITLE))) + .andExpect(jsonPath("$[1].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[1].title", is(TodoConstants.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenSearchTermIsEmpty_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, "") + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenSearchTermIsEmpty_ShouldTwoTodoEntriesAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, "") + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].id", is(SECOND_TODO_ID.intValue()))) + .andExpect(jsonPath("$[0].title", is(SECOND_TODO_TITLE))) + .andExpect(jsonPath("$[1].id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$[1].title", is(TodoConstants.TITLE))); + } +} \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java new file mode 100644 index 0000000..bd7c57e --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java @@ -0,0 +1,327 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITUpdateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void update_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.UPDATED_DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.UPDATED_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase(value = "update-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java new file mode 100644 index 0000000..e410ded --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java @@ -0,0 +1,79 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import net.petrikainulainen.springdata.jpa.web.WebTestConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITGetAuthenticatedUserTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void getAuthenticatedUser_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnUserInformationAsJSON() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.username", is("user"))) + .andExpect(jsonPath("$.role", is(UserRole.ROLE_USER.name()))); + } +} diff --git a/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java new file mode 100644 index 0000000..a7d93ff --- /dev/null +++ b/custom-method-single-repo/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITLoginTest { + + private static final String INVALID_PASSWORD = "invalidPassword"; + private static final String INVALID_USERNAME = "invalidUsername"; + + private static final String PARAM_NAME_PASSWORD = "password"; + private static final String PARAM_NAME_USERNAME = "username"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void logIn_WhenUsernameIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, INVALID_USERNAME) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenPasswordIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, INVALID_PASSWORD) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldReturnResponseStatusFound() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isFound()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldRedirectClientToControllerMethodThatReturnsAuthenticatedUser() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(redirectedUrl("/api/authenticated-user")); + } +} diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml new file mode 100644 index 0000000..45bf713 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml new file mode 100644 index 0000000..12e0c00 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml new file mode 100644 index 0000000..50193f2 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml new file mode 100644 index 0000000..0c1e6bc --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml new file mode 100644 index 0000000..fbb3e27 --- /dev/null +++ b/custom-method-single-repo/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/ant/build.xml b/custom-method-single-repo/src/main/ant/build.xml new file mode 100644 index 0000000..90d4c18 --- /dev/null +++ b/custom-method-single-repo/src/main/ant/build.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java new file mode 100644 index 0000000..6a9566b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * This class obtains the current time by using a {@link DateTimeService} + * object. The reason for this is that we can use a different implementation in our integration tests. + * + * In other words: + *
    + *
  • + * Our application always returns the correct time because it uses the + * {@link CurrentTimeDateTimeService} class. + *
  • + *
  • + * When our integration tests are running, we can return a constant time which gives us the possibility + * to assert the creation and modification times saved to the database. + *
  • + *
+ * + * @author Petri Kainulainen + */ +public class AuditingDateTimeProvider implements DateTimeProvider { + + private final DateTimeService dateTimeService; + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Calendar getNow() { + return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java new file mode 100644 index 0000000..424e1d4 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * This class is used in our integration tests and it always returns the + * same time. This gives us the possibility to verify that the correct + * timestamps are saved to the database. + * + * @author Petri Kainulainen + */ +public class ConstantDateTimeService implements DateTimeService { + + public static final String CURRENT_DATE_AND_TIME = getConstantDateAndTime(); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConstantDateTimeService.class); + + private static String getConstantDateAndTime() { + return "2015-07-19T12:52:28" + + getSystemZoneOffset() + + getSystemZoneId(); + } + + private static String getSystemZoneOffset() { + return ZonedDateTime.now().getOffset().toString(); + } + + private static String getSystemZoneId() { + return "[" + ZoneId.systemDefault().toString() + "]"; + } + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime constantDateAndTime = ZonedDateTime.from(FORMATTER.parse(CURRENT_DATE_AND_TIME)); + + LOGGER.info("Returning constant date and time: {}", constantDateAndTime); + + return constantDateAndTime; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java new file mode 100644 index 0000000..2812fb0 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +/** + * This class returns the current time. + * + * @author Petri Kainulainen + */ +public class CurrentTimeDateTimeService implements DateTimeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CurrentTimeDateTimeService.class); + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime currentDateAndTime = ZonedDateTime.now(); + + LOGGER.info("Returning current date and time: {}", currentDateAndTime); + + return currentDateAndTime; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java new file mode 100644 index 0000000..a1e1a11 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java @@ -0,0 +1,18 @@ +package net.petrikainulainen.springdata.jpa.common; + +import java.time.ZonedDateTime; + +/** + * This interface defines the methods used to get the current + * date and time. + * + * @author Petri Kainulainen + */ +public interface DateTimeService { + + /** + * Returns the current date and time. + * @return + */ + ZonedDateTime getCurrentDateAndTime(); +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java new file mode 100644 index 0000000..46f2849 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * This controller is responsible of starting the frontend application. + * @author Petri Kainulainen + */ +@Controller +public class FrontendLoaderController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FrontendLoaderController.class); + + private static final String FRONTEND_APPLICATION_VIEW = "frontend/client"; + + /** + * Starts the AngularJS application. + * @return + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public String startAngularJSApplication() { + LOGGER.debug("Starting frontend single page application."); + return FRONTEND_APPLICATION_VIEW; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java new file mode 100644 index 0000000..d3cf557 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.common; + +/** + * This class provides static utility methods that are used to ensure that a constructor or a method was invoked properly. + * These methods throw an exception if the specified precondition is violated. + * + * This class selects the thrown exception by using the guideline given in Effective Java by Joshua Bloch (Item 60). + * + * @author Petri Kainulainen + */ +public final class PreCondition { + + private PreCondition() {} + + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessageTemplate The template that is used to construct the message of the exception thrown if the + * inspected exception is false. The template must use the syntax that is supported + * by the {@link java.lang.String#format(String, Object...)} method. + * @param errorMessageArguments The arguments that are used when the message of the thrown exception is constructed. + * @throws java.lang.IllegalArgumentException if the inspected exception is false. + */ + public static void isTrue(boolean expression, String errorMessageTemplate, Object... errorMessageArguments) { + isTrue(expression, String.format(errorMessageTemplate, errorMessageArguments)); + } + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessage The error message that is passed forward to the exception that is thrown + * if the expression is false. + * @throws java.lang.IllegalArgumentException if the inspected expression is false. + */ + public static void isTrue(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the string given as a method parameter is not empty. + * @param string The inspected string. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the string is empty. + * @throws java.lang.IllegalArgumentException if the inspected string is empty. + */ + public static void notEmpty(String string, String errorMessage) { + if (string.isEmpty()) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the object given as a method parameter is not null. + * @param reference A reference to the inspected object. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the object given as a method parameter is null. + * @throws java.lang.NullPointerException If the object given as a method parameter is null. + */ + public static void notNull(Object reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java new file mode 100644 index 0000000..ed511d8 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java @@ -0,0 +1,34 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/** + * This component returns the username of the authenticated user. + * + * @author Petri Kainulainen + */ +public class UsernameAuditorAware implements AuditorAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(UsernameAuditorAware.class); + + @Override + public String getCurrentAuditor() { + LOGGER.debug("Getting the username of authenticated user."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + LOGGER.debug("Current user is anonymous. Returning null."); + return null; + } + + String username = ((User) authentication.getPrincipal()).getUsername(); + LOGGER.debug("Returning username: {}", username); + + return username; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java new file mode 100644 index 0000000..0f922d8 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.common.CurrentTimeDateTimeService; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * @author Petri Kainulainen + */ +@Configuration +@ComponentScan("net.petrikainulainen.springdata.jpa") +@Import({WebMvcContext.class, PersistenceContext.class, SecurityContext.class}) +public class ExampleApplicationContext { + + private static final String MESSAGE_SOURCE_BASE_NAME = "i18n/messages"; + + /** + * These static classes are required because it makes it possible to use + * different properties files for every Spring profile. + * + * See: This StackOverflow answer for more details. + */ + @Profile(Profiles.APPLICATION) + @Configuration + @PropertySource("classpath:application.properties") + static class ApplicationProperties {} + + @Profile(Profiles.APPLICATION) + @Bean + DateTimeService currentTimeDateTimeService() { + return new CurrentTimeDateTimeService(); + } + + @Profile(Profiles.INTEGRATION_TEST) + @Configuration + @PropertySource("classpath:integration-test.properties") + static class IntegrationTestProperties {} + + @Profile(Profiles.INTEGRATION_TEST) + @Bean + DateTimeService constantDateTimeService() { + return new ConstantDateTimeService(); + } + + @Bean + MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(MESSAGE_SOURCE_BASE_NAME); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } + + @Bean + PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java new file mode 100644 index 0000000..6812c6b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java @@ -0,0 +1,146 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.petrikainulainen.springdata.jpa.common.AuditingDateTimeProvider; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import net.petrikainulainen.springdata.jpa.common.UsernameAuditorAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +/** + * This configuration class configures the persistence layer of our example application and + * enables annotation driven transaction management. + * + * This configuration is put to a single class because this way we can write integration + * tests for our persistence layer by using the configuration used by our example + * application. In other words, we can ensure that the persistence layer of our application + * works as expected. + * + * @author Petri Kainulainen + */ +@Configuration +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") +@EnableJpaRepositories(basePackages = { + "net.petrikainulainen.springdata.jpa.todo" +}) +@EnableTransactionManagement +class PersistenceContext { + private static final String[] ENTITY_PACKAGES = { + "net.petrikainulainen.springdata.jpa.todo" + }; + + private static final String PROPERTY_NAME_DB_DRIVER_CLASS = "db.driver"; + private static final String PROPERTY_NAME_DB_PASSWORD = "db.password"; + private static final String PROPERTY_NAME_DB_URL = "db.url"; + private static final String PROPERTY_NAME_DB_USER = "db.username"; + private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; + private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; + private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; + private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; + private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; + + @Bean + AuditorAware auditorProvider() { + return new UsernameAuditorAware(); + } + + @Bean + DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { + return new AuditingDateTimeProvider(dateTimeService); + } + + /** + * Creates and configures the HikariCP datasource bean. + * @param env The runtime environment of our application. + * @return + */ + @Bean(destroyMethod = "close") + DataSource dataSource(Environment env) { + HikariConfig dataSourceConfig = new HikariConfig(); + dataSourceConfig.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DB_DRIVER_CLASS)); + dataSourceConfig.setJdbcUrl(env.getRequiredProperty(PROPERTY_NAME_DB_URL)); + dataSourceConfig.setUsername(env.getRequiredProperty(PROPERTY_NAME_DB_USER)); + dataSourceConfig.setPassword(env.getRequiredProperty(PROPERTY_NAME_DB_PASSWORD)); + + return new HikariDataSource(dataSourceConfig); + } + + /** + * Creates the bean that creates the JPA entity manager factory. + * @param dataSource The datasource that provides the database connections. + * @param env The runtime environment of our application. + * @return + */ + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + entityManagerFactoryBean.setPackagesToScan(ENTITY_PACKAGES); + + Properties jpaProperties = new Properties(); + + //Configures the used database dialect. This allows Hibernate to create SQL + //that is optimized for the used database. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); + + //Specifies the action that is invoked to the database when the Hibernate + //SessionFactory is created or closed. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); + + //Configures the naming strategy that is used when Hibernate creates + //new database objects and schema elements + jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); + + //If the value of this property is true, Hibernate writes all SQL + //statements to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); + + //If the value of this property is true, Hibernate will use prettyprint + //when it writes SQL to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); + + entityManagerFactoryBean.setJpaProperties(jpaProperties); + + return entityManagerFactoryBean; + } + + /** + * Creates the jdbc template bean that we use to invoke SQL queries via JDBC. + * @param dataSource The datasource that provides the database connection. + * @return + */ + @Bean + NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + /** + * Creates the transaction manager bean that integrates the used JPA provider with the + * Spring transaction mechanism. + * @param entityManagerFactory The used JPA entity manager factory. + * @return + */ + @Bean + JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java new file mode 100644 index 0000000..bda9711 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.config; + +/** + * This class defines the Spring profiles used in the project. The idea behind this class + * is that it helps us to avoid typos when we are using these profiles. At the moment + * there are two profiles which are described in the following: + *
    + *
  • The APPLICATION profile is used when we run our example application.
  • + *
  • The INTEGRATION_TEST profile is used when we run the integration tests of our example application.
  • + *
+ * + * @author Petri Kainulainen + */ +public class Profiles { + public static final String APPLICATION = "application"; + public static final String INTEGRATION_TEST = "integrationtest"; + /** + * Prevent instantiation. + */ + private Profiles() {} +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java new file mode 100644 index 0000000..8aa95e4 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.web.security.CsrfHeaderFilter; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationEntryPoint; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationFailureHandler; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationSuccessHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CsrfFilter; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebSecurity +class SecurityContext extends WebSecurityConfigurerAdapter { + + @Bean + AuthenticationEntryPoint authenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + AuthenticationFailureHandler authenticationFailureHandler() { + return new RestAuthenticationFailureHandler(); + } + + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler() { + return new RestAuthenticationSuccessHandler(); + } + + @Bean + protected UserDetailsService userDetailsService() { + return super.userDetailsService(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + //Use the custom authentication entry point. + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint()) + .and() + //Configure form login. + .formLogin() + .loginProcessingUrl("/api/login") + .failureHandler(authenticationFailureHandler()) + .successHandler(authenticationSuccessHandler()) + .permitAll() + .and() + //Configure logout function. + .logout() + .deleteCookies("JSESSIONID") + .logoutUrl("/api/logout") + .logoutSuccessUrl("/") + .and() + //Configure url based authorization + .authorizeRequests() + .antMatchers( + "/", + "/api/csrf" + ).permitAll() + .anyRequest().hasRole("USER") + .and() + .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + //Spring Security ignores request to static resources such as CSS or JS files. + .ignoring() + .antMatchers( + "/favicon.ico", + "/css/**", + "/i18n/**", + "/js/**" + ); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java new file mode 100644 index 0000000..f1861a6 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import java.util.EnumSet; + +/** + * @author Petri Kainulainen + */ +public class WebAppConfig implements WebApplicationInitializer { + private static final String CHARACTER_ENCODING_FILTER_ENCODING = "UTF-8"; + private static final String CHARACTER_ENCODING_FILTER_NAME = "characterEncoding"; + private static final String CHARACTER_ENCODING_FILTER_URL_PATTERN = "/*"; + + private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; + private static final String DISPATCHER_SERVLET_MAPPING = "/"; + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(ExampleApplicationContext.class); + + //XmlWebApplicationContext rootContext = new XmlWebApplicationContext(); + //rootContext.setConfigLocation("classpath:applicationContext.xml"); + + configureDispatcherServlet(servletContext, rootContext); + EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + + configureCharacterEncodingFilter(servletContext, dispatcherTypes); + configureSpringSecurityFilter(servletContext, dispatcherTypes); + servletContext.addListener(new ContextLoaderListener(rootContext)); + } + + private void configureDispatcherServlet(ServletContext servletContext, WebApplicationContext rootContext) { + ServletRegistration.Dynamic dispatcher = servletContext.addServlet( + DISPATCHER_SERVLET_NAME, + new DispatcherServlet(rootContext) + ); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); + } + + private void configureCharacterEncodingFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + characterEncodingFilter.setEncoding(CHARACTER_ENCODING_FILTER_ENCODING); + characterEncodingFilter.setForceEncoding(true); + FilterRegistration.Dynamic characterEncoding = servletContext.addFilter(CHARACTER_ENCODING_FILTER_NAME, characterEncodingFilter); + characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, CHARACTER_ENCODING_FILTER_URL_PATTERN); + } + + private void configureSpringSecurityFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); + security.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java new file mode 100644 index 0000000..c016860 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebMvc +class WebMvcContext extends WebMvcConfigurerAdapter { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + + @Override + public void configureMessageConverters(List> converters) { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + + converters.add(converter); + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp("/WEB-INF/jsp/", ".jsp"); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/CustomTodoRepository.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/CustomTodoRepository.java new file mode 100644 index 0000000..17007ca --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/CustomTodoRepository.java @@ -0,0 +1,19 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This interface declares the custom methods that can be added into a Spring + * Data JPA repository interface by extending this interface. + * + * @author Petri Kainulainen + */ +interface CustomTodoRepository { + + /** + * Finds todo entries by using the search term given as a method parameter. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. + */ + List findBySearchTerm(String searchTerm); +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java new file mode 100644 index 0000000..a6ea6f1 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoSearchService implements TodoSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoSearchService.class); + + private final TodoRepository repository; + + @Autowired + public RepositoryTodoSearchService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + @Override + public List findBySearchTerm(String searchTerm) { + LOGGER.info("Finding todo entries by search term: {}", searchTerm); + + List searchResults = repository.findBySearchTerm(searchTerm); + LOGGER.info("Found {} todo entries", searchResults.size()); + + return searchResults; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java new file mode 100644 index 0000000..f59be0d --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoService implements TodoCrudService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoService.class); + + private final TodoRepository repository; + + @Autowired + RepositoryTodoService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional + @Override + public TodoDTO create(TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + Todo created = Todo.getBuilder() + .description(newTodoEntry.getDescription()) + .title(newTodoEntry.getTitle()) + .build(); + + created = repository.save(created); + LOGGER.info("Created a new todo entry: {}", created); + + return TodoMapper.mapEntityIntoDTO(created); + } + + @Transactional + @Override + public TodoDTO delete(Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + Todo deleted = findTodoEntryById(id); + LOGGER.debug("Found todo entry: {}", deleted); + + repository.delete(deleted); + LOGGER.info("Deleted todo entry: {}", deleted); + + return TodoMapper.mapEntityIntoDTO(deleted); + } + + @Transactional(readOnly = true) + @Override + public List findAll() { + LOGGER.info("Finding all todo entries."); + + List todoEntries = repository.findAll(); + + LOGGER.info("Found {} todo entries", todoEntries.size()); + + return TodoMapper.mapEntitiesIntoDTOs(todoEntries); + } + + @Transactional(readOnly = true) + @Override + public TodoDTO findById(Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + Todo todoEntry = findTodoEntryById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return TodoMapper.mapEntityIntoDTO(todoEntry); + } + + @Transactional + @Override + public TodoDTO update(TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + Todo updated = findTodoEntryById(updatedTodoEntry.getId()); + updated.update(updatedTodoEntry.getTitle(), updatedTodoEntry.getDescription()); + + //We need to flush the changes or otherwise the returned object + //doesn't contain the updated audit information. + repository.flush(); + + LOGGER.info("Updated the information of the todo entry: {}", updated); + + return TodoMapper.mapEntityIntoDTO(updated); + } + + private Todo findTodoEntryById(Long id) { + Optional todoResult = repository.findOne(id); + return todoResult.orElseThrow(() -> new TodoNotFoundException(id)); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java new file mode 100644 index 0000000..b3acfbb --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java @@ -0,0 +1,196 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.annotations.Type; +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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Version; +import java.time.ZonedDateTime; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.isTrue; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This entity class contains the information of a single todo entry + * and the methods that are used to create new todo entries and to modify + * the information of an existing todo entry. + * + * @author Petri Kainulainen + */ +@Entity +@EntityListeners(AuditingEntityListener.class) +@NamedNativeQuery(name = "Todo.findBySearchTermNamedNative", + query="SELECT * FROM todos t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC", + resultClass = Todo.class +) +@NamedQuery(name = "Todo.findBySearchTermNamed", + query = "SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) " + + "ORDER BY t.title ASC" +) +@Table(name = "todos") +final class Todo { + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(name = "created_by_user", nullable = false) + @CreatedBy + private String createdByUser; + + @Column(name = "creation_time", nullable = false) + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @CreatedDate + private ZonedDateTime creationTime; + + @Column(name = "description", length = MAX_LENGTH_DESCRIPTION) + private String description; + + @Column(name = "modified_by_user", nullable = false) + @LastModifiedBy + private String modifiedByUser; + + @Column(name = "modification_time") + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @LastModifiedDate + private ZonedDateTime modificationTime; + + @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE) + private String title; + + @Version + private long version; + + /** + * Required by Hibernate. + */ + private Todo() {} + + private Todo(Builder builder) { + this.title = builder.title; + this.description = builder.description; + } + + static Builder getBuilder() { + return new Builder(); + } + + Long getId() { + return id; + } + + String getCreatedByUser() { + return createdByUser; + } + + ZonedDateTime getCreationTime() { + return creationTime; + } + + String getDescription() { + return description; + } + + String getModifiedByUser() { + return modifiedByUser; + } + + ZonedDateTime getModificationTime() { + return modificationTime; + } + + String getTitle() { + return title; + } + + long getVersion() { + return version; + } + + void update(String newTitle, String newDescription) { + requireValidTitleAndDescription(newTitle, newDescription); + + this.title = newTitle; + this.description = newDescription; + } + + private void requireValidTitleAndDescription(String title, String description) { + notNull(title, "Title cannot be null."); + notEmpty(title, "Title cannot be empty."); + isTrue(title.length() <= MAX_LENGTH_TITLE, + "The maximum length of the title is <%d> characters.", + MAX_LENGTH_TITLE + ); + + isTrue((description == null) || (description.length() <= MAX_LENGTH_DESCRIPTION), + "The maximum length of the description is <%d> characters.", + MAX_LENGTH_DESCRIPTION + ); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .append("version", this.version) + .toString(); + } + + /** + * This entity is so simple that you don't really need to use the builder pattern + * (use a constructor instead). I use the builder pattern here because it makes + * the code a bit more easier to read. + */ + static class Builder { + private String description; + private String title; + + private Builder() {} + + Builder description(String description) { + this.description = description; + return this; + } + + Builder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = new Todo(this); + + build.requireValidTitleAndDescription(build.getTitle(), build.getDescription()); + + return build; + } + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java new file mode 100644 index 0000000..9e6fc09 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java @@ -0,0 +1,49 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +public interface TodoCrudService { + + /** + * Creates a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + TodoDTO create(TodoDTO newTodoEntry); + + /** + * Deletes a todo entry from the database. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + TodoDTO delete(Long id); + + /** + * Finds all todo entries that are saved to the database. + * @return + */ + List findAll(); + + /** + * Finds a todo entry by using the id given as a method parameter. + * @param id The id of the wanted todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO findById(Long id); + + /** + * Updates the information of an existing information. + * @param updatedTodoEntry The new information of an existing todo entry. + * @return The information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO update(TodoDTO updatedTodoEntry); +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java new file mode 100644 index 0000000..7eea8d2 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Size; +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTO { + + private String createdByUser; + + private ZonedDateTime creationTime; + + @Size(max = Todo.MAX_LENGTH_DESCRIPTION) + private String description; + + private Long id; + + private String modifiedByUser; + + private ZonedDateTime modificationTime; + + @NotEmpty + @Size(max = Todo.MAX_LENGTH_TITLE) + private String title; + + public TodoDTO() {} + + public String getCreatedByUser() { + return createdByUser; + } + + public ZonedDateTime getCreationTime() { + return creationTime; + } + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + public String getModifiedByUser() { + return modifiedByUser; + } + + public ZonedDateTime getModificationTime() { + return modificationTime; + } + + public String getTitle() { + return title; + } + + public void setCreatedByUser(String createdByUser) { + this.createdByUser = createdByUser; + } + + public void setCreationTime(ZonedDateTime creationTime) { + this.creationTime = creationTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setId(Long id) { + this.id = id; + } + + public void setModifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + + public void setModificationTime(ZonedDateTime modificationTime) { + this.modificationTime = modificationTime; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .toString(); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java new file mode 100644 index 0000000..aa36e28 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Petri Kainulainen + */ +final class TodoMapper { + + static List mapEntitiesIntoDTOs(List entities) { + return entities.stream() + .map(TodoMapper::mapEntityIntoDTO) + .collect(toList()); + } + + static TodoDTO mapEntityIntoDTO(Todo entity) { + TodoDTO dto = new TodoDTO(); + + dto.setCreatedByUser(entity.getCreatedByUser()); + dto.setCreationTime(entity.getCreationTime()); + dto.setDescription(entity.getDescription()); + dto.setId(entity.getId()); + dto.setModifiedByUser(entity.getModifiedByUser()); + dto.setModificationTime(entity.getModificationTime()); + dto.setTitle(entity.getTitle()); + + return dto; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java new file mode 100644 index 0000000..63f6948 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * This exception is thrown when a todo entry is not found by + * using the given id. + * + * @author Petri Kainulainen + */ +public class TodoNotFoundException extends RuntimeException { + + private final Long id; + + public TodoNotFoundException(Long id) { + super(); + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java new file mode 100644 index 0000000..b91d3ed --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * This repository provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +interface TodoRepository extends Repository, CustomTodoRepository { + + void delete(Todo deleted); + + List findAll(); + + Optional findOne(Long id); + + void flush(); + + Todo save(Todo persisted); +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepositoryImpl.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepositoryImpl.java new file mode 100644 index 0000000..3a49ab1 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepositoryImpl.java @@ -0,0 +1,52 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Petri Kainulainen + */ +@Repository +final class TodoRepositoryImpl implements CustomTodoRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoRepositoryImpl.class); + + private static final String SEARCH_TODO_ENTRIES = "SELECT id, title FROM todos t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC"; + + private final NamedParameterJdbcTemplate jdbcTemplate; + + @Autowired + TodoRepositoryImpl(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional(readOnly = true) + @Override + public List findBySearchTerm(String searchTerm) { + LOGGER.info("Finding todo entries by using search term: {}", searchTerm); + + Map queryParams = new HashMap<>(); + queryParams.put("searchTerm", searchTerm); + + List searchResults = jdbcTemplate.query(SEARCH_TODO_ENTRIES, + queryParams, + new BeanPropertyRowMapper<>(TodoSearchResultDTO.class) + ); + + LOGGER.info("Found {} todo entries", searchResults.size()); + + return searchResults; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java new file mode 100644 index 0000000..63dcd6c --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchResultDTO.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * @author Petri Kainulainen + */ +public final class TodoSearchResultDTO { + + private Long id; + + private String title; + + public TodoSearchResultDTO() {} + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setId(Long id) { + this.id = id; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java new file mode 100644 index 0000000..50efcd2 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java @@ -0,0 +1,19 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides finder methods for {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +public interface TodoSearchService { + + /** + * Finds todo entries whose title or description contains the given search term. + * This search is case insensitive. + * @param searchTerm The search term. + * @return A list of todo entries whose title or description contains the given search term. + */ + public List findBySearchTerm(String searchTerm); +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java new file mode 100644 index 0000000..5205657 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java @@ -0,0 +1,116 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * This controller provides the public API that is used to perform + * CRUD operations for todo entries. + * + * @author Petri Kainulainen + */ +@RestController +@RequestMapping("/api/todo") +final class TodoController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoController.class); + + private final TodoCrudService crudService; + + @Autowired + TodoController(TodoCrudService crudService) { + this.crudService = crudService; + } + + /** + * Create a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + TodoDTO create(@RequestBody @Valid TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + TodoDTO created = crudService.create(newTodoEntry); + LOGGER.info("Created a new todo entry: {}", created); + + return created; + } + + /** + * Deletes a todo entry. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public TodoDTO delete(@PathVariable("id") Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + TodoDTO deleted = crudService.delete(id); + LOGGER.info("Deleted the todo entry: {}", deleted); + + return deleted; + } + + /** + * Finds all todo entries. + * + * @return The information of all todo entries. + */ + @RequestMapping(method = RequestMethod.GET) + List findAll() { + LOGGER.info("Finding all todo entries"); + + List todoEntries = crudService.findAll(); + LOGGER.info("Found {} todo entries.", todoEntries.size()); + + return todoEntries; + } + + /** + * Finds a single todo entry. + * @param id The id of the requested todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.GET) + TodoDTO findById(@PathVariable("id") Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + TodoDTO todoEntry = crudService.findById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return todoEntry; + } + + /** + * Updates the information of an existing todo entry. + * @param updatedTodoEntry The new information of the updated todo entry. + * @return The updated information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.PUT) + TodoDTO update(@RequestBody @Valid TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + TodoDTO updated = crudService.update(updatedTodoEntry); + LOGGER.info("Updated the information of the todo entrY: {}", updated); + + return updated; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java new file mode 100644 index 0000000..270565e --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoSearchResultDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * This controller provides the public API that is used to find todo entries by using + * different search criteria. + * + * @author Petri Kainulainen + */ +@RestController +final class TodoSearchController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoSearchController.class); + + private final TodoSearchService searchService; + + @Autowired + public TodoSearchController(TodoSearchService searchService) { + this.searchService = searchService; + } + + /** + * Finds todo entries whose title or description contains the given search term. This + * search is case insensitive. + * @param searchTerm The used search term. + * @return + */ + @RequestMapping(value = "/api/todo/search", method = RequestMethod.GET) + public List findBySearchTerm(@RequestParam("searchTerm") String searchTerm) { + LOGGER.info("Finding todo entries by search term: {}", searchTerm); + + List searchResults = searchService.findBySearchTerm(searchTerm); + LOGGER.info("Found {} todo entries", searchResults.size()); + + return searchResults; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java new file mode 100644 index 0000000..b02059e --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of an error that occurred when the API tried + * to perform the operation requested by the client. + * + * @author Petri Kainulainen + */ +final class ErrorDTO { + + private final String code; + private final String message; + + ErrorDTO(String code, String message) { + notNull(code, "Code cannot be null."); + notEmpty(code, "Code cannot be empty."); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty"); + + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java new file mode 100644 index 0000000..44234a5 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of a single field error. + * + * @author Petri Kainulainen + */ +final class FieldErrorDTO { + + private final String field; + + private final String message; + + FieldErrorDTO(String field, String message) { + notNull(field, "Field cannot be null."); + notEmpty(field, "Field cannot be empty"); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty."); + + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java new file mode 100644 index 0000000..5ad9e9b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; +import java.util.Locale; + +/** + * This class handles the exceptions thrown by our REST API. + * + * @author Petri Kainulainen + */ +@ControllerAdvice +public final class RestErrorHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class); + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + + private final MessageSource messageSource; + + @Autowired + public RestErrorHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * Processes an error that occurs when the requested todo entry is not found. + * @param ex The exception that was thrown when the todo entry was not found. + * @param currentLocale The current locale. + * @return An error object that contains the error code and message. + */ + @ExceptionHandler(TodoNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + ErrorDTO handleTodoEntryNotFound(TodoNotFoundException ex, Locale currentLocale) { + LOGGER.error("Todo entry was not found by using id: {}", ex.getId()); + + MessageSourceResolvable errorMessageRequest = createSingleErrorMessageRequest( + ERROR_CODE_TODO_ENTRY_NOT_FOUND, + ex.getId() + ); + + String errorMessage = messageSource.getMessage(errorMessageRequest, currentLocale); + return new ErrorDTO(HttpStatus.NOT_FOUND.name(), errorMessage); + } + + private DefaultMessageSourceResolvable createSingleErrorMessageRequest(String errorMessageCode, Object... params) { + return new DefaultMessageSourceResolvable(new String[] {errorMessageCode}, params); + } + + /** + * Processes an error that occurs when the validation of an object fails. + * + * @param ex The exception that was thrown when the validation failed. + * @return An error object that describes all validation errors. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ValidationErrorDTO handleValidationErrors(MethodArgumentNotValidException ex, Locale currentLocale) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result.getFieldErrors(); + LOGGER.error("Found {} validation errors", fieldErrors.size()); + + return constructValidationErrors(fieldErrors, currentLocale); + } + + private ValidationErrorDTO constructValidationErrors(List fieldErrors, Locale currentLocale) { + ValidationErrorDTO dto = new ValidationErrorDTO(); + + for (FieldError fieldError: fieldErrors) { + String localizedErrorMessage = getValidationErrorMessage(fieldError, currentLocale); + dto.addFieldError(fieldError.getField(), localizedErrorMessage); + } + + return dto; + } + + private String getValidationErrorMessage(FieldError fieldError, Locale currentLocale) { + String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale); + + //If the message was not found, return the most accurate field error code instead. + //You can remove this check if you prefer to get the default error message. + if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) { + String[] fieldErrorCodes = fieldError.getCodes(); + localizedErrorMessage = fieldErrorCodes[0]; + } + + return localizedErrorMessage; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java new file mode 100644 index 0000000..8355c7b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import org.springframework.http.HttpStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class contains the information of validation errors that are found + * from a controller method parameter that is annotated with the + * {@link javax.validation.Valid} annotation. + * + * @author Petri Kainulainen + */ +final class ValidationErrorDTO { + + private final String code = HttpStatus.BAD_REQUEST.name(); + + private final List fieldErrors = new ArrayList<>(); + + ValidationErrorDTO() { + } + + void addFieldError(String field, String message) { + FieldErrorDTO error = new FieldErrorDTO(field, message); + fieldErrors.add(error); + } + + public String getCode() { + return code; + } + + public List getFieldErrors() { + return fieldErrors; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java new file mode 100644 index 0000000..141a948 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java @@ -0,0 +1,46 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This filter reads the {@link org.springframework.security.web.csrf.CsrfToken} from the {@link HttpServletRequest} and + * sets its content to the {@link HttpServletResponse} headers. + * + * I borrowed this idea from this StackOverflow question. + * + * @author Petri Kainulainen + */ +public class CsrfHeaderFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(CsrfHeaderFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + LOGGER.trace("Reading CSRF token from the request."); + + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (token != null) { + LOGGER.trace("CSRF token was found. Creating HTTP response headers."); + response.setHeader("X-CSRF-HEADER", token.getHeaderName()); + response.setHeader("X-CSRF-PARAM", token.getParameterName()); + response.setHeader("X-CSRF-TOKEN", token.getToken()); + } + else { + LOGGER.trace("CSRF Token was not found. Doing nothing."); + } + + filterChain.doFilter(request, response); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java new file mode 100644 index 0000000..f6e70cb --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Petri Kainulainen + */ +@RestController +public class CsrfTokenController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsrfTokenController.class); + + @RequestMapping(value = "/api/csrf", method = RequestMethod.HEAD) + public void getCsrfToken() { + LOGGER.info("Getting CSRF token."); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..887e25b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication entry point returns the HTTP status code 401. + * @author Petri Kainulainen + */ +public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + LOGGER.info("Authentication required. Returning HTTP status code 401."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..daf635b --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication failure handler returns the HTTP status code 403. + * @author Petri Kainulainen + */ +public final class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationFailureHandler.class); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + LOGGER.info("Authentication failed with message: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failed."); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ff84785 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication success handler returns the information of the authenticated + * user as JSON. + * + * @author Petri Kainulainen + */ +public final class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + LOGGER.info("Authentication was successful"); + response.sendRedirect(response.encodeRedirectURL("/api/authenticated-user")); + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java new file mode 100644 index 0000000..ef7959d --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java @@ -0,0 +1,45 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to return the information + * of the authenticated user. + * + * @author Petri Kainulainen + */ +@RestController +final class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + /** + * Returns the information of the authenticated user as JSON. The returned information + * contains the username and the user role of the authenticated user. + * + * @param authenticatedUser The information of the authenticated user. + * @return + */ + @RequestMapping(value = "/api/authenticated-user", method = RequestMethod.GET) + public UserDTO getAuthenticatedUser(@AuthenticationPrincipal User authenticatedUser) { + LOGGER.info("Getting authenticated user."); + + if (authenticatedUser == null) { + //If anonymous users can access this controller method, someone has changed + //the security configuration and it must be fixed. + LOGGER.error("Authenticated user is not found."); + throw new AccessDeniedException("Anonymous users cannot request the information of the authenticated user."); + } + else { + LOGGER.info("User with username: {} is authenticated", authenticatedUser.getUsername()); + return new UserDTO(authenticatedUser.getUsername(), authenticatedUser.getAuthorities()); + } + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java new file mode 100644 index 0000000..92b99ed --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import net.petrikainulainen.springdata.jpa.common.PreCondition; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * This class contains the information of the authenticated user. + * + * @author Petri Kainulainen + */ +public final class UserDTO { + + private final String username; + + private final UserRole role; + + UserDTO(String username, Collection authorities) { + PreCondition.isTrue(!username.isEmpty(), "Username cannot be empty."); + PreCondition.isTrue(authorities.size() == 1, "User must have only one granted authority."); + this.username = username; + + GrantedAuthority authority = authorities.iterator().next(); + this.role = UserRole.valueOf(authority.getAuthority()); + } + + public String getUsername() { + return username; + } + + public UserRole getRole() { + return role; + } +} diff --git a/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java new file mode 100644 index 0000000..8b3e6a6 --- /dev/null +++ b/custom-method-single-repo/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java @@ -0,0 +1,8 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +/** + * @author Petri Kainulainen + */ +enum UserRole { + ROLE_USER +} diff --git a/custom-method-single-repo/src/main/resources/META-INF/jpa-named-queries.properties b/custom-method-single-repo/src/main/resources/META-INF/jpa-named-queries.properties new file mode 100644 index 0000000..97d737e --- /dev/null +++ b/custom-method-single-repo/src/main/resources/META-INF/jpa-named-queries.properties @@ -0,0 +1,2 @@ +Todo.findBySearchTermNamedFile=SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC +Todo.findBySearchTermNamedNativeFile=SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/META-INF/orm.xml b/custom-method-single-repo/src/main/resources/META-INF/orm.xml new file mode 100644 index 0000000..cc2bf80 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/META-INF/orm.xml @@ -0,0 +1,16 @@ + + + + + SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC + + + + SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/application.properties b/custom-method-single-repo/src/main/resources/application.properties new file mode 100644 index 0000000..7ac8298 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/application.properties @@ -0,0 +1,12 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/applicationContext-persistence.xml b/custom-method-single-repo/src/main/resources/applicationContext-persistence.xml new file mode 100644 index 0000000..e9149e9 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/applicationContext-persistence.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${hibernate.dialect} + + + ${hibernate.hbm2ddl.auto} + + + ${hibernate.ejb.naming_strategy} + + + ${hibernate.show_sql} + + + ${hibernate.format_sql} + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/applicationContext-web.xml b/custom-method-single-repo/src/main/resources/applicationContext-web.xml new file mode 100644 index 0000000..db48af6 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/applicationContext-web.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + WRITE_DATES_AS_TIMESTAMPS + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/applicationContext.xml b/custom-method-single-repo/src/main/resources/applicationContext.xml new file mode 100644 index 0000000..b9ee424 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/applicationContext.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/i18n/messages.properties b/custom-method-single-repo/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..0e376f5 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/i18n/messages.properties @@ -0,0 +1,5 @@ +error.todo.entry.not.found=No todo entry was found by using id: {0} + +NotEmpty.todoDTO.title=The title cannot be empty +Size.todoDTO.description=The maximum length of description is 500 characters +Size.todoDTO.title=The maximum length of title is 100 characters \ No newline at end of file diff --git a/custom-method-single-repo/src/main/resources/integration-test.properties b/custom-method-single-repo/src/main/resources/integration-test.properties new file mode 100644 index 0000000..3605c55 --- /dev/null +++ b/custom-method-single-repo/src/main/resources/integration-test.properties @@ -0,0 +1,14 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa;DB_CLOSE_ON_EXIT=FALSE +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false + +test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1 \ No newline at end of file diff --git a/tutorial-part-six/src/main/resources/log4j.properties b/custom-method-single-repo/src/main/resources/log4j.properties similarity index 75% rename from tutorial-part-six/src/main/resources/log4j.properties rename to custom-method-single-repo/src/main/resources/log4j.properties index 5ad34eb..668d97a 100644 --- a/tutorial-part-six/src/main/resources/log4j.properties +++ b/custom-method-single-repo/src/main/resources/log4j.properties @@ -3,4 +3,6 @@ log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG + +log4j.logger.org.hibernate=INFO +log4j.logger.org.springframework=INFO \ No newline at end of file diff --git a/custom-method-single-repo/src/main/webapp/WEB-INF/jsp/frontend/client.jsp b/custom-method-single-repo/src/main/webapp/WEB-INF/jsp/frontend/client.jsp new file mode 100644 index 0000000..84158d0 --- /dev/null +++ b/custom-method-single-repo/src/main/webapp/WEB-INF/jsp/frontend/client.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ +
+
+

+

+
+
+ + + diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java new file mode 100644 index 0000000..7e90183 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Petri Kainulainen + */ +public class PreConditionTest { + + private static final String STATIC_ERROR_MESSAGE = "static error message"; + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, "Dynamic error message with parameter: %d", 1L); + } + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic error message with parameter: 1"); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, STATIC_ERROR_MESSAGE); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsNotEmpty_ShouldNotThrowException() { + PreCondition.notEmpty(" ", STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsEmpty_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNotNull_ShouldNotThrowException() { + PreCondition.notNull(new Object(), STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNull_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java new file mode 100644 index 0000000..2f902c5 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java @@ -0,0 +1,78 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoSearchServiceTest { + + private static final String SEARCH_TERM = "itl"; + + private TodoRepository repository; + private RepositoryTodoSearchService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoSearchService(repository); + } + + public class FindBySearchTerm { + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + given(repository.findBySearchTerm(SEARCH_TERM)).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List searchResults = service.findBySearchTerm(SEARCH_TERM); + assertThat(searchResults).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + private final Long ID = 20L; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoSearchResultDTO found = new TodoSearchResultDTO(); + found.setId(ID); + found.setTitle(TITLE); + + given(repository.findBySearchTerm(SEARCH_TERM)).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List searchResults = service.findBySearchTerm(SEARCH_TERM); + assertThat(searchResults).hasSize(1); + } + + @Test + public void shouldReturnTheInformationOfOneTodoEntry() { + TodoSearchResultDTO found = service.findBySearchTerm(SEARCH_TERM).get(0); + + assertThat(found.getId()).isEqualTo(ID); + assertThat(found.getTitle()).isEqualTo(TITLE); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java new file mode 100644 index 0000000..81bbdb5 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java @@ -0,0 +1,387 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoServiceTest { + + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + private static final Long ID = 20L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + private TodoRepository repository; + + private RepositoryTodoService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoService(repository); + } + + public class Create { + + @Before + public void returnNewTodoEntry() { + given(repository.save(isA(Todo.class))).willAnswer( + invocationOnMock -> new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build() + ); + } + + @Test + public void shouldPersistNewTodoEntryWithCorrectInformation() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + service.create(newTodoEntry); + + verify(repository, times(1)).save( + assertArg(persisted -> assertThatTodoEntry(persisted) + .hasNoCreationAuditFieldValues() + .hasDescription(DESCRIPTION) + .hasNoId() + .hasNoModificationAuditFieldValues() + .hasTitle(TITLE) + ) + ); + verifyNoMoreInteractions(repository); + } + + @Test + public void shouldReturnTheInformationOfPersistedTodoEntry() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + TodoDTO created = service.create(newTodoEntry); + assertThatTodoDTO(created) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.delete(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException ex = (TodoNotFoundException) thrown; + assertThat(ex.getId()).isEqualTo(ID); + } + + @Test + public void shouldNotDeleteTodoEntry() { + catchThrowable(() -> service.delete(ID)); + + verify(repository, never()).delete(isA(Todo.class)); + } + } + + public class WhenTodoEntryIsFound { + + private Todo deleted; + + @Before + public void returnDeletedTodoEntry() { + deleted = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(deleted)); + } + + @Test + public void shouldDeleteFoundTodoEntry() { + service.delete(ID); + + verify(repository, times(1)).delete(deleted); + } + + @Test + public void shouldReturnTheInformationOfDeletedTodoEntry() { + TodoDTO deleted = service.delete(ID); + + assertThatTodoDTO(deleted) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindAll { + + public class WhenNoTodoEntryAreFound { + + @Before + public void returnNoTodoEntries() { + given(repository.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).hasSize(1); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO todoEntry = service.findAll().get(0); + + assertThatTodoDTO(todoEntry) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindOne { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.findById(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(found)); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO returned = service.findById(ID); + + assertThatTodoDTO(returned) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + Throwable thrown = catchThrowable(() -> service.update(updatedTodoEntry)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + private Todo updated; + + @Before + public void returnUpdatedTodoEntry() { + updated = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(updated)); + } + + @Test + public void shouldUpdateTitleAndDescription() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldNotUpdateIdOrAuditInformation() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasId(ID) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntry() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + TodoDTO returnedTodoEntry = service.update(updatedTodoEntry); + + assertThatTodoDTO(returnedTodoEntry) + .hasDescription(UPDATED_DESCRIPTION) + .hasId(ID) + .hasTitle(UPDATED_TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java new file mode 100644 index 0000000..ed98667 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author Petri Kainulainen + */ +public final class TestUtil { + + private TestUtil() {} + + public static String createStringWithLength(int length) { + StringBuilder string = new StringBuilder(); + + for (int index = 0; index < length; index++) { + string.append("a"); + } + + return string.toString(); + } + + public static ZonedDateTime parseDateTime(String dateAndTime) { + return ZonedDateTime.parse(dateAndTime, DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java new file mode 100644 index 0000000..e88f27c --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java @@ -0,0 +1,198 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This class provides a fluent API that can be used for writing assertions + * to {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +final class TodoAssert extends AbstractAssert { + + private TodoAssert(Todo actual) { + super(actual, TodoAssert.class); + } + + static TodoAssert assertThatTodoEntry(Todo actual) { + return new TodoAssert(actual); + } + + TodoAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage(String.format( + "Expected description to be <%s> but was <%s>.", + expectedDescription, + actualDescription + )) + .isEqualTo(expectedDescription); + + return this; + } + + TodoAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creationTime to be but was <%s>", + actualCreationTime + ) + .isNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + return this; + } + + TodoAssert hasNoDescription() { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage("Expected description to be but was <%s>", actualDescription) + .isNull(); + + return this; + } + + TodoAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be <%d> but was <%d>", + expectedId, + actualId + ) + .isEqualTo(expectedId); + + return this; + } + + TodoAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>.", actualId) + .isNull(); + + return this; + } + + TodoAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>.", + actualModificationTime + ) + .isNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>", + actualModificationTime + ) + .isNull(); + + return this; + } + + TodoAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>.", + expectedTitle, + actualTitle + ) + .isEqualTo(actualTitle); + + return this; + } + + public TodoAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java new file mode 100644 index 0000000..90ee955 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java @@ -0,0 +1,71 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +class TodoBuilder { + + private Long id; + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + TodoBuilder() {} + + TodoBuilder id(Long id) { + this.id = id; + return this; + } + + TodoBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + TodoBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + TodoBuilder description(String description) { + this.description = description; + return this; + } + + TodoBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + TodoBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + TodoBuilder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = Todo.getBuilder() + .title(title) + .description(description) + .build(); + + ReflectionTestUtils.setField(build, "createdByUser", createdByUser); + ReflectionTestUtils.setField(build, "creationTime", creationTime); + ReflectionTestUtils.setField(build, "id", id); + ReflectionTestUtils.setField(build, "modifiedByUser", modifiedByUser); + ReflectionTestUtils.setField(build, "modificationTime", modificationTime); + + return build; + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java new file mode 100644 index 0000000..462d90d --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java @@ -0,0 +1,179 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTOAssert extends AbstractAssert { + + private TodoDTOAssert(TodoDTO actual) { + super(actual, TodoDTOAssert.class); + } + + public static TodoDTOAssert assertThatTodoDTO(TodoDTO actual) { + return new TodoDTOAssert(actual); + } + + public TodoDTOAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage( + "Expected description to be <%s> but was <%s>", + expectedDescription, + actualDescription + ) + .isEqualTo(expectedDescription); + + return this; + } + + public TodoDTOAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage( + "Expected id to be <%d> but was <%d>", + actualId, + expectedId + ) + .isEqualTo(expectedId); + + return this; + } + + public TodoDTOAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage("Expected creationTime to be but was <%s>", actualCreationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>", actualId) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be but was <%s>", + actualModifiedByUser + ) + .isNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage("Expected modification time to be but was <%d>", actualModificationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>", + expectedTitle, + actualTitle + ) + .isEqualTo(expectedTitle); + + return this; + } + + public TodoDTOAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoDTOAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoDTOAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoDTOAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java new file mode 100644 index 0000000..e0b5505 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java @@ -0,0 +1,68 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public class TodoDTOBuilder { + + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private Long id; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + public TodoDTOBuilder() {} + + public TodoDTOBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + public TodoDTOBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + public TodoDTOBuilder description(String description) { + this.description = description; + return this; + } + + public TodoDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public TodoDTOBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + public TodoDTOBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + public TodoDTOBuilder title(String title) { + this.title = title; + return this; + } + + public TodoDTO build() { + TodoDTO build = new TodoDTO(); + + build.setCreatedByUser(createdByUser); + build.setCreationTime(creationTime); + build.setDescription(description); + build.setId(id); + build.setModifiedByUser(modifiedByUser); + build.setModificationTime(modificationTime); + build.setTitle(title); + + return build; + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java new file mode 100644 index 0000000..c5ff69d --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java @@ -0,0 +1,340 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoTest { + + private static final int MAX_LENGTH_DESCRIPTION = 500; + private static final int MAX_LENGTH_TITLE = 100; + + private static final String DESCRIPTION = "description"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + public class Build { + + public class WhenTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(null) + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title("") + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(tooLongTitle) + .description(DESCRIPTION) + .build(); + } + } + } + + public class WhenDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(TITLE) + .description(tooLongDescription) + .build(); + } + } + + public class WhenTitleAndDescriptionAreValid { + + @Test + public void shouldNotSetId() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoId(); + } + + @Test + public void shouldNotSetCreationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoCreationAuditFieldValues(); + } + + @Test + public void shouldNotSetModificationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoModificationAuditFieldValues(); + } + + @Test + public void shouldSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasDescription(DESCRIPTION); + } + + @Test + public void shouldSetTitle() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(TITLE); + } + + public class WhenMaxLengthTitleIsGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldCreateNewObjectAndSetTitle() { + Todo build = Todo.getBuilder() + .title(maxLengthTitle) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(maxLengthTitle); + } + } + + public class WhenMaxLengthDescriptionIsGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + + } + + @Test + public void shouldCreateNewObjectAndSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(maxLengthDescription) + .build(); + + assertThatTodoEntry(build) + .hasDescription(maxLengthDescription); + } + } + + public class WhenNoDescriptionIsGiven { + + @Test + public void shouldCreateNewObjectWithoutDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .build(); + + assertThatTodoEntry(build) + .hasNoDescription(); + } + } + } + } + + public class Update { + + private Todo updated; + + @Before + public void createUpdatedTodoEntry() { + updated = Todo.getBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + } + + public class WhenNewTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + updated.update(null, UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update("", UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(tooLongTitle, UPDATED_DESCRIPTION); + } + } + } + + public class WhenNewDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(UPDATED_TITLE, tooLongDescription); + } + } + + public class WhenNewTitleAndNewDescriptionAreValid { + + public class WhenMaxLengthTitleAndNewDescriptionAreGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldUpdateTitle() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasTitle(maxLengthTitle); + } + + @Test + public void shouldUpdateDescription() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION); + } + } + + public class WhenNewTitleIsGivenAndNewDescriptionIsNull { + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldRemoveDescription() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasNoDescription(); + } + } + + public class WhenNewTitleAndMaxLengthDescriptionAreGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + } + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldUpdateDescription() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasDescription(maxLengthDescription); + } + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java new file mode 100644 index 0000000..a0dd3eb --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java @@ -0,0 +1,691 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoControllerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + + private static final String ERROR_MESSAGE_KEY_MISSING_TITLE = "NotEmpty.todoDTO.title"; + private static final String ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION = "Size.todoDTO.description"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_TITLE = "Size.todoDTO.title"; + + private static final Long ID = 1L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private static final String TITLE = "title"; + + private MockMvc mockMvc; + + private TodoCrudService crudService; + + private StaticMessageSource messageSource; + + @Before + public void setUp() { + crudService = mock(TodoCrudService.class); + + messageSource = new StaticMessageSource(); + messageSource.setUseCodeAsDefaultMessage(true); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(crudService)) + .setHandlerExceptionResolvers(WebTestConfig.restErrorHandler(messageSource)) + .setLocaleResolver(WebTestConfig.fixedLocaleResolver(CURRENT_LOCALE)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setValidator(WebTestConfig.validator()) + .build(); + } + + public class Create { + + public class WhenTodoEntryIsNotValid { + + public class WhenTodoEntryIsEmpty { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + private TodoDTO newTodoEntry; + + @Before + public void createInputAndReturnNewTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + newTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .title(maxLengthTitle) + .build(); + + TodoDTO created = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.create(isA(TodoDTO.class))).willReturn(created); + } + + @Test + public void shouldReturnResponseStatusCreated() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isCreated()); + } + + @Test + public void shouldReturnCreatedTodoEntryAsJson() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldCreateNewTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verify(crudService, times(1)).create( + assertArg(created -> assertThatTodoDTO(created) + .hasDescription(maxLengthDescription) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoId() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwNotFoundException() { + given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnDeletedTodoEntry() { + TodoDTO deleted = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.delete(ID)).willReturn(deleted); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class FindAll { + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnNoTodoEntries() { + given(crudService.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } + + public class FindById { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findById(ID)).willReturn(found); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + public class WhenTodoEntryIsNotValid { + + public class WhenTitleAndDescriptionAreMissing { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + TodoDTO updatedTodoEntry; + + @Before + public void createInputAndReturnUpdatedTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + updatedTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .id(ID) + .title(maxLengthTitle) + .build(); + + TodoDTO updated = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.update(isA(TodoDTO.class))).willReturn(updated); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldUpdateTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verify(crudService, times(1)).update( + assertArg(updated -> assertThatTodoDTO(updated) + .hasDescription(maxLengthDescription) + .hasId(ID) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java new file mode 100644 index 0000000..6c6a787 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java @@ -0,0 +1,111 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchResultDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoSearchControllerTest { + + private static final String SEARCH_TERM = "itl"; + + private MockMvc mockMvc; + + private TodoSearchService searchService; + + @Before + public void setUp() { + searchService = mock(TodoSearchService.class); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoSearchController(searchService)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setCustomArgumentResolvers(WebTestConfig.sortArgumentResolver()) + .build(); + } + + public class FindBySearchTerm { + + @Test + public void shouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldPassSearchTermForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ); + + verify(searchService, times(1)).findBySearchTerm(eq(SEARCH_TERM)); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + given(searchService.findBySearchTerm(SEARCH_TERM)).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + private final Long ID= 1L; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoSearchResultDTO found = new TodoSearchResultDTO(); + found.setId(ID); + found.setTitle(TITLE); + + given(searchService.findBySearchTerm(SEARCH_TERM)).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java new file mode 100644 index 0000000..a099861 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java @@ -0,0 +1,111 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import net.petrikainulainen.springdata.jpa.web.error.RestErrorHandler; +import org.springframework.context.MessageSource; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.FixedLocaleResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Locale; + +/** + * This factory class provides methods that can be used to create objects that are useful + * when we are writing unit tests for our controller methods by using the Spring MVC Test + * framework. + * + * @author Petri Kainulainen + */ +final class WebTestConfig { + + private WebTestConfig() {} + + /** + * Configures a {@link org.springframework.web.servlet.LocaleResolver} that always returns the + * configured {@link java.util.Locale}. + * + * @return + */ + static LocaleResolver fixedLocaleResolver(Locale fixedLocale) { + return new FixedLocaleResolver(fixedLocale); + } + + /** + * This method creates a custom {@link org.springframework.http.converter.HttpMessageConverter} which ensures that: + * + *
    + *
  • Null values are ignored.
  • + *
  • + * The new Java 8 date objects are serialized in standard + * ISO-8601 string representation. + *
  • + *
+ * + * @return + */ + static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return converter; + } + + /** + * This method ensures that the {@link RestErrorHandler} class + * is used to handle the exceptions thrown by the tested controller. I borrowed this idea from + * this StackOverflow answer. + * + * @return an error handler component that delegates relevant exceptions forward to the {@link RestErrorHandler} class. + */ + static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) { + final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() { + @Override + protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerMethod handlerMethod, + final Exception exception) { + Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception); + if (method != null) { + return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method); + } + return super.getExceptionHandlerMethod(handlerMethod, exception); + } + }; + exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter())); + exceptionResolver.afterPropertiesSet(); + return exceptionResolver; + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Sort} objects by using the request params of the + * incoming request. + * @return + */ + static SortHandlerMethodArgumentResolver sortArgumentResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + /** + * This method creates a validator object that adds support for bean validation API 1.0 and 1.1. + * + * @return The created validator object. + */ + static LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java new file mode 100644 index 0000000..2246b2b --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java @@ -0,0 +1,32 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.http.MediaType; + +import java.nio.charset.Charset; + +/** + * @author Petri Kainulainen + */ +public final class WebTestConstants { + + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), + MediaType.APPLICATION_JSON.getSubtype(), + Charset.forName("utf8") + ); + + static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + static final String ERROR_CODE_VALIDATION_FAILED = "BAD_REQUEST"; + + static final String FIELD_NAME_DESCRIPTION = "description"; + static final String FIELD_NAME_TITLE = "title"; + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + static final String REQUEST_PARAM_SEARCH_TERM = "searchTerm"; + + /** + * Prevents instantiation. + */ + private WebTestConstants() {} +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java new file mode 100644 index 0000000..9340fe6 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +/** + * @author Petri Kainulainen + */ +final class WebTestUtil { + + /** + * Prevents instantiation + */ + private WebTestUtil() {} + + /** + * Transforms an object into JSON and returns the JSON as a byte array. + * @param object The object that is transformed into JSON. + * @return The JSON representation of an object as a byte array. + * @throws IOException + */ + static byte[] convertObjectToJsonBytes(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsBytes(object); + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java new file mode 100644 index 0000000..528a552 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.ErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ErrorDTOTest { + + private static final String CODE = "code"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenCodeIsInvalid { + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenCodeIsNull() { + new ErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenCodeIsEmpty() { + new ErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new ErrorDTO(CODE, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new ErrorDTO(CODE, ""); + } + } + + public class WhenCodeAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetCode() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getCode()).isEqualTo(CODE); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java new file mode 100644 index 0000000..25fc6bf --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java @@ -0,0 +1,64 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class FieldErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenFieldIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenFieldIsNull() { + new FieldErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenFieldIsEmpty() { + new FieldErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new FieldErrorDTO(FIELD, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new FieldErrorDTO(FIELD, ""); + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetField() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java new file mode 100644 index 0000000..5ff8f34 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java @@ -0,0 +1,242 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.MethodParameter; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.List; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RestErrorHandlerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + + private static final Long TODO_ID = 99L; + + private MessageSource messageSource; + + private RestErrorHandler errorHandler; + + @Before + public void setUp() { + messageSource = mock(MessageSource.class); + this.errorHandler = new RestErrorHandler(messageSource); + } + + public class HandleTodoEntryNotFound { + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + + private static final String ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 99"; + + @Before + public void returnErrorMessageNotFound() { + given(messageSource.getMessage( + isA(MessageSourceResolvable.class), + isA(Locale.class)) + ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldFindErrorMessageByUsingCurrentLocale() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage(isA(MessageSourceResolvable.class), eq(CURRENT_LOCALE)); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectId() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getArguments()) + .containsOnly(TODO_ID) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectMessageCode() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getCodes()) + .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldReturnErrorThatHasCorrectErrorCode() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getCode()).isEqualTo(ERROR_CODE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldReturnErrorThatHasCorrectMessage() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getMessage()).isEqualTo(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + } + + public class HandleValidationErrors { + + private static final String ERROR_CODE_VALIDATION_ERROR = "BAD_REQUEST"; + private static final String ERROR_MESSAGE_VALIDATION_ERROR = "validationError"; + + private static final String FIELD_DEFAULT_MESSAGE = "DefaultMessage"; + private static final String FIELD_WITH_VALIDATION_ERROR = "field"; + private static final String OBJECT_WITH_VALIDATION_ERROR = "todoDTO"; + + private static final String VALIDATION_ERROR_CODE_ACCURATE = "Error"; + private static final String VALIDATION_ERROR_CODE_LESS_ACCURATE = "Maybe"; + + public class WhenOneValidationErrorIsFound { + + public class WhenMessageIsFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(ERROR_MESSAGE_VALIDATION_ERROR); + } + } + + public class WhenMessageIsNotFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnDefaultErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasFieldErrorWithMostAccurateFieldErrorCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(VALIDATION_ERROR_CODE_ACCURATE); + } + } + } + + private MethodArgumentNotValidException createExceptionWithFieldErrors(FieldError... fieldErrors) { + BindingResult bindingResult = new BeanPropertyBindingResult(new TodoDTO(), OBJECT_WITH_VALIDATION_ERROR); + + for (FieldError fieldError: fieldErrors) { + bindingResult.addError(fieldError); + } + + return new MethodArgumentNotValidException(mock(MethodParameter.class), bindingResult); + } + + + private final class FieldErrorBuilder { + + private String defaultMessage; + private String[] errorCodes; + private String fieldName; + + private FieldErrorBuilder() {} + + private FieldErrorBuilder defaultMessage(String defaultMessage) { + this.defaultMessage = defaultMessage; + return this; + } + + private FieldErrorBuilder errorCodes(String... errorCodes) { + this.errorCodes = errorCodes; + return this; + } + + private FieldErrorBuilder fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + private FieldError build() { + return new FieldError(OBJECT_WITH_VALIDATION_ERROR, + fieldName, + null, + false, + errorCodes, + new Object[]{}, + defaultMessage + ); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java new file mode 100644 index 0000000..8ae069a --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import net.petrikainulainen.springdata.jpa.web.error.ValidationErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ValidationErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class AddFieldError { + + public class WhenFieldIsInvalid { + + public class WhenFieldIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(null, MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(null, MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenFieldIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError("", MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError("", MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenMessageIsInvalid { + + public class WhenMessageIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, null); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, null)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenMessageIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, ""); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, "")); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewFieldErrorAndSetField() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewFieldErrorAndSetMessage() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java new file mode 100644 index 0000000..1928461 --- /dev/null +++ b/custom-method-single-repo/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class UserDTOTest { + + public class CreateNew { + + private final String ROLE_USER = UserRole.ROLE_USER.name(); + + public class WhenUsernameIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER); + new UserDTO("", authorities); + } + } + + public class WhenUserNameIsNotEmpty { + + private final String USERNAME = "username"; + + public class WhenUserHasNoGrantedAuthorities { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + new UserDTO(USERNAME, new ArrayList<>()); + } + } + + public class WhenUserHasOneGrantedAuthority { + + public class WhenGrantedAuthorityIsKnown { + + private Collection authorities; + + @Before + public void createKnownAuthority() { + authorities = createAuthorities(ROLE_USER); + } + + @Test + public void shouldSetUsername() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getUsername()).isEqualTo(USERNAME); + } + + @Test + public void shouldSetRole() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getRole()).isEqualTo(UserRole.ROLE_USER); + } + } + + public class WhenGrantedAuthorityIsUnknown { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities("UNKNOWN_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + + public class WhenUserHasMoreThanOneGrantedAuthority { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER, "ANOTHER_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + } + + private Collection createAuthorities(String... roles) { + List authorities = new ArrayList<>(); + + for (String role: roles) { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); + authorities.add(authority); + } + + return authorities; + } +} diff --git a/integration-testing/src/integration-test/java/net/petrikainulainen/spring/datajpa/todo/repository/ITTodoRepositoryTest.java b/integration-testing/src/integration-test/java/net/petrikainulainen/spring/datajpa/todo/repository/ITTodoRepositoryTest.java index 1b5fa7c..7a1b857 100644 --- a/integration-testing/src/integration-test/java/net/petrikainulainen/spring/datajpa/todo/repository/ITTodoRepositoryTest.java +++ b/integration-testing/src/integration-test/java/net/petrikainulainen/spring/datajpa/todo/repository/ITTodoRepositoryTest.java @@ -32,7 +32,7 @@ DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) -@DatabaseSetup("todoData.xml") +@DatabaseSetup("toDoData.xml") public class ITTodoRepositoryTest { @Autowired diff --git a/query-methods/.gitignore b/query-methods/.gitignore index 1c28079..02895f1 100644 --- a/query-methods/.gitignore +++ b/query-methods/.gitignore @@ -4,4 +4,7 @@ *.iml build h2db -target \ No newline at end of file +target +node_modules +bower_components +build \ No newline at end of file diff --git a/query-methods/README.md b/query-methods/README.md index 0ff8c05..3ba8a24 100644 --- a/query-methods/README.md +++ b/query-methods/README.md @@ -3,7 +3,13 @@ This blog post is the example application of the following blog posts: * [Spring Data JPA Tutorial: Getting the Required Dependencies](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-getting-the-required-dependencies/) * [Spring Data JPA Tutorial: Configuration](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/) * [Spring Data JPA Tutorial: CRUD](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/) -* Spring Data JPA Tutorial: Query Methods (TBD) +* [Spring Data JPA Tutorial: Introduction to Query Methods](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-introduction-to-query-methods/) +* [Spring Data JPA Tutorial: Creating Database Queries From Method Names](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-from-method-names/) +* [Spring Data JPA Tutorial: Creating Database Queries With the @Query Annotation](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-the-query-annotation/) +* [Spring Data JPA Tutorial: Creating Database Queries With Named Queries](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-named-queries/) +* [Spring Data JPA Tutorial: Auditing, Part One](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-one/) +* [Spring Data JPA Tutorial: Auditing, Part Two](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-two/) +* [Spring Data JPA Tutorial: Sorting]() - Not published yet **Note:** This application is still work in progress. @@ -12,9 +18,34 @@ Prerequisites You need to install the following tools if you want to run this application: +Backend +--------- + * [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) * [Maven](http://maven.apache.org/) (the application is tested with Maven 3.2.1) +Frontend +---------- + +* [Node.js](http://nodejs.org/) +* [NPM](https://www.npmjs.org/) +* [Bower](http://bower.io/) +* [Gulp](http://gulpjs.com/) + +You can install these tools by following these steps: + +1. Install Node.js by using a [downloaded binary](http://nodejs.org/download/) or a [package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). + You can also read this blog post: [How to install Node.js and NPM](http://blog.nodeknockout.com/post/65463770933/how-to-install-node-js-and-npm) + +2. Install Bower by using the following command: + + npm install -g bower + +3. Install Gulp by using the following command: + + npm install -g gulp + + Running the Tests ================= @@ -32,3 +63,14 @@ Running the Application You can run the application by using the following command: mvn clean jetty:run -P dev + +Credits +========= + +* Kyösti Herrala. The Gulp build script and its Maven integration are based on Kyösti's ideas. +* [Techniques for authentication in AngularJS applications](https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec) + +Known Issues +============ + +* If you refresh the login page, you aren't redirected away from it after successful login. \ No newline at end of file diff --git a/query-methods/frontend/.bowerrc b/query-methods/frontend/.bowerrc new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/query-methods/frontend/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file diff --git a/query-methods/frontend/.jshintrc b/query-methods/frontend/.jshintrc new file mode 100644 index 0000000..f648d46 --- /dev/null +++ b/query-methods/frontend/.jshintrc @@ -0,0 +1,33 @@ +{ + "globalstrict": true, + "browser": true, + "devel": true, + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "globals": { + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "angular": true, + "jQuery": true, + "_": true, + "$": true + } +} \ No newline at end of file diff --git a/query-methods/frontend/app/app.js b/query-methods/frontend/app/app.js new file mode 100644 index 0000000..074fd4f --- /dev/null +++ b/query-methods/frontend/app/app.js @@ -0,0 +1,98 @@ +'use strict'; + +var App = angular.module('app', [ + 'angular-logger', + 'http-auth-interceptor', + 'ngLocale', + 'ngCookies', + 'ngResource', + 'ngSanitize', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.router', + 'ui.utils', + 'angular-growl', + 'angularMoment', + 'angularUtils.directives.dirPagination', + 'spring-security-csrf-token-interceptor', + + //Partials + 'templates', + + //Account + 'app.account.config', 'app.account.directives', 'app.account.controllers', 'app.account.services', + + //Common + 'app.common.config', 'app.common.controllers', 'app.common.directives', 'app.common.services', + + //Todo + 'app.todo.controllers', 'app.todo.directives', 'app.todo.services', + + //Search + 'app.search.controllers', 'app.search.directives', 'app.search.services' + +]); + +App.run(['$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', 'authService', 'AuthenticationService', 'COMMON_EVENTS', + function ($log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser, authService, AuthenticationService, COMMON_EVENTS) { + + var logger = $log.getInstance('app'); + + //This function retries all requests that were failed because of + //the 401 response. + function listenAuthenticationEvents() { + var confirmLogin = function() { + authService.loginConfirmed(); + }; + + $rootScope.$on(AUTH_EVENTS.loginSuccess, confirmLogin); + + var viewLogInPage = function() { + logger.info('User is not authenticated. Rendering login view.'); + $state.go('todo.login'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthenticated, viewLogInPage); + + var viewTodoListPage = function() { + logger.info("User logged out. REndering todo list view."); + $state.go('todo.list', {}, {reload: true}); + }; + + $rootScope.$on(AUTH_EVENTS.logoutSuccess, viewTodoListPage); + + var viewForbiddenPage = function() { + logger.info('Permission was denied for user: %j', AuthenticatedUser); + $state.go('todo.forbidden'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthorized, viewForbiddenPage); + } + + function listenCommonEvents() { + + var view404Page = function() { + logger.info('Requested page was not found.'); + $state.go('todo.404'); + }; + + $rootScope.$on(COMMON_EVENTS.notFound, view404Page); + } + + //This function ensures that anonymous users cannot access states + //that marked as protected (i.e. the value of the authenticated + //property is set to true). + function secureProtectedStates() { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + logger.trace('Moving to state: %s', toState.name); + AuthenticationService.authorizeStateChange(event, toState, toParams); + }); + } + + $rootScope.currentUser = AuthenticatedUser; + + listenAuthenticationEvents(); + listenCommonEvents(); + secureProtectedStates(); + }]); + diff --git a/query-methods/frontend/app/assets/i18n/en.json b/query-methods/frontend/app/assets/i18n/en.json new file mode 100644 index 0000000..869176b --- /dev/null +++ b/query-methods/frontend/app/assets/i18n/en.json @@ -0,0 +1,101 @@ +{ + "app.title.label": "Spring Data JPA Tutorial - Query Methods", + "dialogs": { + "delete.dialog": { + "cancel.button.label": "Cancel", + "delete.button.label": "Delete", + "text": "Are you sure that you want to delete the todo entry with title: {{title}}?", + "title": "Delete todo entry?" + } + }, + "directives": { + "login.form": { + "login.button": "Login", + "login.failed": "Login failed!" + }, + "log.out.link.label": "Log Out", + "todo.form": { + "cancel.button": "Cancel", + "save.button": "Save" + } + }, + "footer.message": "Spring Data JPA example application by Petri Kainulainen", + "header.brand.label": "Spring Data JPA Tutorial", + "pages": { + "add.page": { + "title": "Add new todo entry", + "link.label": "Add new todo entry" + }, + "delete.link": "Delete", + "edit.page": { + "link.label": "Edit", + "title": "Edit todo entry" + }, + "forbidden.page": { + "text": "Permission denied.", + "title": "Forbidden" + }, + "not.found.page": { + "text": "The page that you were looking for was not found.", + "title": "Not Found" + }, + "list.page": { + "title": "Things to do", + "texts": { + "no.todo.entries.found": "Nothing to do (yet)." + } + }, + "login.page": { + "title": "Log In" + }, + "search.results.page": { + "texts": { + "no.todo.entries.found": "No todo entries was found with the given search term." + }, + "title": "Search Results" + }, + "view.page": { + "title": "View Todo Entry" + } + }, + "login": { + "help": "Log in by using username: 'user' and password: 'password'", + "username": "Username", + "username.placeholder": "Enter username", + "password": "Password", + "password.placeholder": "Enter password" + }, + "search": { + "term.field.placeholder": "Search", + "missing.characters.text": "{{missingCharCount}} characters missing" + }, + "todo": { + "created.by.prefix": "by", + "creation.time": "Created at", + "description": "Description", + "description.placeholder": "Enter description", + "messages": { + "description.maxLength": "Description cannot be longer than 500 characters", + "title.maxLength": "Title cannot be longer than 100 characters", + "title.required": "Title is required" + }, + "modified.by.prefix": "by", + "modification.time": "Modified at", + "notifications": { + "add": { + "error": "Adding a new todo entry failed.", + "success": "A new todo entry was added." + }, + "delete": { + "error": "Deleting the todo entry failed.", + "success": "Deleted the todo entry." + }, + "edit": { + "error": "Updating the information of a todo entry failed.", + "success": "Updated the information of the todo entry." + } + }, + "title": "Title", + "title.placeholder": "Enter title" + } +} \ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/account/forbidden-view.html b/query-methods/frontend/app/assets/partials/account/forbidden-view.html new file mode 100644 index 0000000..c761f3e --- /dev/null +++ b/query-methods/frontend/app/assets/partials/account/forbidden-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/account/login-form-directive.html b/query-methods/frontend/app/assets/partials/account/login-form-directive.html new file mode 100644 index 0000000..d2f14aa --- /dev/null +++ b/query-methods/frontend/app/assets/partials/account/login-form-directive.html @@ -0,0 +1,36 @@ +
+ + +
+ +
+
+ : + +
+
+ : + +
+
+ +
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/account/login-view.html b/query-methods/frontend/app/assets/partials/account/login-view.html new file mode 100644 index 0000000..199d339 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/account/login-view.html @@ -0,0 +1,6 @@ +

+ +
+
+

+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/account/logout-link-directive.html b/query-methods/frontend/app/assets/partials/account/logout-link-directive.html new file mode 100644 index 0000000..4d9550a --- /dev/null +++ b/query-methods/frontend/app/assets/partials/account/logout-link-directive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/common/not-found-view.html b/query-methods/frontend/app/assets/partials/common/not-found-view.html new file mode 100644 index 0000000..7edf553 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/common/not-found-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/search/dirPagination.tpl.html b/query-methods/frontend/app/assets/partials/search/dirPagination.tpl.html new file mode 100644 index 0000000..558aa20 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/search/dirPagination.tpl.html @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/search/search-form-directive.html b/query-methods/frontend/app/assets/partials/search/search-form-directive.html new file mode 100644 index 0000000..674143e --- /dev/null +++ b/query-methods/frontend/app/assets/partials/search/search-form-directive.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/search/search-result-list-directive.html b/query-methods/frontend/app/assets/partials/search/search-result-list-directive.html new file mode 100644 index 0000000..c38f4f7 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/search/search-result-list-directive.html @@ -0,0 +1,19 @@ +
+ + +
+ +
+
+
+

+
diff --git a/query-methods/frontend/app/assets/partials/search/search-result-view.html b/query-methods/frontend/app/assets/partials/search/search-result-view.html new file mode 100644 index 0000000..2d8cd39 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/search/search-result-view.html @@ -0,0 +1,4 @@ +
+

+
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/add-todo-view.html b/query-methods/frontend/app/assets/partials/todo/add-todo-view.html new file mode 100644 index 0000000..0a0406a --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/add-todo-view.html @@ -0,0 +1,9 @@ +

+ +
+
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/delete-todo-modal.html b/query-methods/frontend/app/assets/partials/todo/delete-todo-modal.html new file mode 100644 index 0000000..b390319 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/delete-todo-modal.html @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/edit-todo-view.html b/query-methods/frontend/app/assets/partials/todo/edit-todo-view.html new file mode 100644 index 0000000..1695ae6 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/edit-todo-view.html @@ -0,0 +1,8 @@ +

+
+
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/todo-form-directive.html b/query-methods/frontend/app/assets/partials/todo/todo-form-directive.html new file mode 100644 index 0000000..c7815d0 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/todo-form-directive.html @@ -0,0 +1,52 @@ +
+
+ : + +
+ + +
+
+
+ : + +
+ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/todo-list-directive.html b/query-methods/frontend/app/assets/partials/todo/todo-list-directive.html new file mode 100644 index 0000000..60ed955 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/todo-list-directive.html @@ -0,0 +1,8 @@ +
+

+
+ diff --git a/query-methods/frontend/app/assets/partials/todo/todo-list-view.html b/query-methods/frontend/app/assets/partials/todo/todo-list-view.html new file mode 100644 index 0000000..6a83ba4 --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/todo-list-view.html @@ -0,0 +1,7 @@ +
+

+ +
+ +
+
\ No newline at end of file diff --git a/query-methods/frontend/app/assets/partials/todo/view-todo-view.html b/query-methods/frontend/app/assets/partials/todo/view-todo-view.html new file mode 100644 index 0000000..374c16d --- /dev/null +++ b/query-methods/frontend/app/assets/partials/todo/view-todo-view.html @@ -0,0 +1,25 @@ +
+

+ +
+

{{todoEntry.title}}

+

{{todoEntry.description}}

+
+

+ + {{"todo.creation.time" | translate}}: {{todoEntry.creationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.created.by.prefix" | translate}} {{todoEntry.createdByUser}} + {{"todo.modification.time" | translate }}: {{todoEntry.modificationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.modified.by.prefix" | translate}} {{todoEntry.modifiedByUser}} + +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/query-methods/frontend/app/module/account/account.config.js b/query-methods/frontend/app/module/account/account.config.js new file mode 100644 index 0000000..c689bb1 --- /dev/null +++ b/query-methods/frontend/app/module/account/account.config.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('app.account.config', []) + .constant('AUTH_EVENTS', { + loginSuccess: 'event:auth-login-success', + loginFailed: 'event:auth-login-failed', + logoutSuccess: 'event:auth-logout-success', + sessionTimeout: 'event:auth-session-timeout', + notAuthenticated: 'event:auth-loginRequired', + notAuthorized: 'event:auth-forbidden' + }) + .config(['csrfProvider', function(csrfProvider) { + // optional configurations + csrfProvider.config({ + httpTypes: ['PUT', 'POST', 'DELETE'], + maxRetries: 1, + url: '/api/csrf' + }); + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/account/account.controllers.js b/query-methods/frontend/app/module/account/account.controllers.js new file mode 100644 index 0000000..79f6fbe --- /dev/null +++ b/query-methods/frontend/app/module/account/account.controllers.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('app.account.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.login', { + url: 'login', + controller: 'LoginController', + templateUrl: 'account/login-view.html' + }) + .state('todo.forbidden', { + url: 'forbidden', + controller: 'ForbiddenController', + templateUrl: 'account/forbidden-view.html' + }); + } + ]) + .controller('ForbiddenController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.ForbiddenController'); + logger.info("Rendering forbidden view."); + }]) + .controller('LoginController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.LoginController'); + logger.info('Rendering login form.'); + }]); + diff --git a/query-methods/frontend/app/module/account/account.directives.js b/query-methods/frontend/app/module/account/account.directives.js new file mode 100644 index 0000000..2ff0aa6 --- /dev/null +++ b/query-methods/frontend/app/module/account/account.directives.js @@ -0,0 +1,44 @@ +'use strict'; + +angular.module('app.account.directives', []) + .directive('logOutLink', ['$log', 'AuthenticationService', function ($log, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.logOutLink'); + + return { + link: function (scope, element, attr) { + scope.logOut = function() { + logger.info('Logging user out.'); + AuthenticationService.logOut(); + }; + }, + templateUrl: 'account/logout-link-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('loginForm', ['$log', 'AUTH_EVENTS', 'AuthenticationService', function ($log, AUTH_EVENTS, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.loginForm'); + + return { + link: function (scope, element, attr) { + scope.login = {}; + scope.loginFailed = false; + + scope.$on(AUTH_EVENTS.loginFailed, function() { + logger.info('Received login failed event.'); + scope.loginFailed = true; + }); + + scope.submitLoginForm = function() { + logger.info('Submitting log in form.'); + AuthenticationService.logIn(scope.login.username, scope.login.password); + }; + }, + templateUrl: 'account/login-form-directive.html', + scope: { + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/account/account.services.js b/query-methods/frontend/app/module/account/account.services.js new file mode 100644 index 0000000..7cbd82d --- /dev/null +++ b/query-methods/frontend/app/module/account/account.services.js @@ -0,0 +1,79 @@ +'use strict'; + +angular.module('app.account.services', ['ngResource']) + .service('AuthenticatedUser', function () { + this.create = function (username, role) { + this.username = username; + this.role = role; + }; + this.destroy = function () { + this.username = null; + this.role = null; + }; + }) + .factory('AuthenticationService', ['$http', '$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', + function($http, $log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser) { + + var logger = $log.getInstance('app.account.services.AuthenticationService'); + + return { + authorizeStateChange: function(event, toState, toParams) { + logger.debug('Authorizing state change to state: %s', toState.name); + if (toState.authenticate && !this.isAuthenticated()) { + event.preventDefault(); + + logger.debug('Authentication is not found. Fetching it from the backend.'); + var self = this; + $http.get('/api/authenticated-user').success(function(user) { + logger.debug('Found authenticated user: %j', user); + AuthenticatedUser.create(user.username, user.role); + + if (!self.isAuthenticated) { + logger.debug('Unauthenticated users is: %j', AuthenticatedUser); + $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); + } + else { + logger.debug('User is authenticated. Continuing to the target state: %s', toState.name); + $state.go(toState.name, toParams); + } + }); + } + }, + isAuthenticated: function() { + logger.debug('Checking if user: %j is authenticated.', AuthenticatedUser); + return AuthenticatedUser.username; + }, + logIn: function(username, password) { + logger.info('Logging in user with username: %s', username); + + var transform = function(data){ + return $.param(data); + }; + + $http.post('/api/login', {username: username, password: password}, { + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + ignoreAuthModule: true, + transformRequest: transform + }) + .success(function(user) { + logger.info('Login successful for user: %j', user); + AuthenticatedUser.create(user.username, user.role); + $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); + }) + .error(function() { + logger.info('Login failed'); + $rootScope.$broadcast(AUTH_EVENTS.loginFailed); + }); + }, + logOut: function() { + if (this.isAuthenticated()) { + $http.post('/api/logout', {}) + .success(function() { + logger.info('User is logged out.'); + AuthenticatedUser.destroy(); + $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); + }); + } + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/common/common.config.js b/query-methods/frontend/app/module/common/common.config.js new file mode 100644 index 0000000..6ffca02 --- /dev/null +++ b/query-methods/frontend/app/module/common/common.config.js @@ -0,0 +1,60 @@ +'use strict'; + +angular.module('app.common.config', []) + .constant('COMMON_EVENTS', { + notFound: 'event:not-found' + }) + .config(['logEnhancerProvider', function (logEnhancerProvider) { + logEnhancerProvider.datetimePattern = 'DD.MM.YYYY HH:mm:ss'; + logEnhancerProvider.prefixPattern = '%s::[%s]> '; + logEnhancerProvider.logLevels = { + '*': logEnhancerProvider.LEVEL.OFF + }; + }]) + .config(['$urlRouterProvider', '$locationProvider', + function ($urlRouterProvider, $locationProvider) { + //this prevents infinite $digest loop when we invoke the + //preventDefault() method in $stateChangeStart event handler. + //See: https://github.com/angular-ui/ui-router/issues/600#issuecomment-47228922 + $urlRouterProvider.otherwise( function($injector, $location) { + var $state = $injector.get("$state"); + $state.go("todo.list"); + }); + + // Without server side support html5 must be disabled. + $locationProvider.html5Mode(false); + } + ]) + .config(['$translateProvider', function ($translateProvider) { + // Initialize angular-translate + $translateProvider.useStaticFilesLoader({ + prefix: '/i18n/', + suffix: '.json' + }); + + $translateProvider.preferredLanguage('en'); + $translateProvider.useSanitizeValueStrategy('escaped'); + $translateProvider.useLocalStorage(); + $translateProvider.useMissingTranslationHandlerLog(); + }]) + .config(['growlProvider', function (growlProvider) { + growlProvider.globalTimeToLive(5000); + }]) + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push([ + '$injector', + function ($injector) { + return $injector.get('404Interceptor'); + } + ]); + }]) + .factory('404Interceptor', ['$rootScope', '$q', 'COMMON_EVENTS', function ($rootScope, $q, COMMON_EVENTS) { + return { + responseError: function(response) { + if (response.status === 404) { + $rootScope.$broadcast(COMMON_EVENTS.notFound); + } + return $q.reject(response); + } + }; + }]); diff --git a/query-methods/frontend/app/module/common/common.controllers.js b/query-methods/frontend/app/module/common/common.controllers.js new file mode 100644 index 0000000..811f15e --- /dev/null +++ b/query-methods/frontend/app/module/common/common.controllers.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('app.common.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.404', { + url: 'not-found', + controller: 'NotFoundController', + templateUrl: 'common/not-found-view.html' + }); + } + ]) + .controller('NotFoundController', ['$log', function($log) { + var logger = $log.getInstance('app.common.controllers.NotFoundController'); + logger.info("Rendering 404 view."); + }]); + diff --git a/query-methods/frontend/app/module/common/common.directives.js b/query-methods/frontend/app/module/common/common.directives.js new file mode 100644 index 0000000..7c56027 --- /dev/null +++ b/query-methods/frontend/app/module/common/common.directives.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('app.common.directives', []) + .directive('staticInclude', ['$http', '$templateCache', '$compile', function ($http, $templateCache, $compile) { + return function(scope, element, attrs) { + var templatePath = attrs.staticInclude; + + $http.get(templatePath, {cache: $templateCache}).success(function (response) { + var contents = $('
').html(response).contents(); + element.html(contents); + $compile(contents)(scope); + }); + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/common/common.services.js b/query-methods/frontend/app/module/common/common.services.js new file mode 100644 index 0000000..de9d0e6 --- /dev/null +++ b/query-methods/frontend/app/module/common/common.services.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('app.common.services', []) + .service('NotificationService', ['$rootScope', 'growl', function ($rootScope, growl) { + var flashMessageQueue = []; + + function displayNotification(message, type) { + if (type === 'success') { + growl.success(message); + } else if (type === 'warn') { + growl.warning(message); + } else if (type === 'info') { + growl.info(message); + } else { + growl.error(message); + } + } + + // Display all flash notifications after state has changed + $rootScope.$on("$stateChangeSuccess", function () { + while (flashMessageQueue.length > 0) { + var item = flashMessageQueue.shift(); + if (item) { + displayNotification(item.message, item.type); + } + } + }); + + // Public API + return { + 'flashMessage': function (message, type) { + flashMessageQueue.push({message: message, type: type || 'info'}); + } + }; + }]); diff --git a/query-methods/frontend/app/module/search/search.controllers.js b/query-methods/frontend/app/module/search/search.controllers.js new file mode 100644 index 0000000..6fb86a5 --- /dev/null +++ b/query-methods/frontend/app/module/search/search.controllers.js @@ -0,0 +1,41 @@ +'use strict'; + +angular.module('app.search.controllers', []) + .constant('paginationConfig', { + firstPageNumber: 1, + pageSize: 5 + }) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.search', { + authenticate: true, + url: 'todo/search/:searchTerm/page/:pageNumber/size/:pageSize', + controller: 'SearchResultController', + templateUrl: 'search/search-result-view.html', + resolve: { + searchResults: ['TodoSearchService', '$stateParams', function(TodoSearchService, $stateParams) { + if ($stateParams.searchTerm) { + return TodoSearchService.findBySearchTerm($stateParams.searchTerm, + $stateParams.pageNumber - 1, + $stateParams.pageSize + ); + } + + return null; + }], + searchTerm: ['$stateParams', function($stateParams) { + return $stateParams.searchTerm; + }] + } + }); + } + ]) + .controller('SearchResultController', ['$log', '$scope', '$state', 'paginationConfig', 'searchResults', 'searchTerm', + function($log, $scope, $state, paginationConfig, searchResults, searchTerm) { + var logger = $log.getInstance('app.search.controllers.SearchResultController'); + logger.info('Rendering search results page for search term: %s with search results: %j', searchTerm, searchResults); + $scope.searchResults = searchResults; + $scope.searchTerm = searchTerm; + }]); + diff --git a/query-methods/frontend/app/module/search/search.directives.js b/query-methods/frontend/app/module/search/search.directives.js new file mode 100644 index 0000000..ecefb9c --- /dev/null +++ b/query-methods/frontend/app/module/search/search.directives.js @@ -0,0 +1,104 @@ +'use strict'; + +angular.module('app.search.directives', []) + .directive('searchForm', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + + var logger = $log.getInstance('app.search.directives.searchForm'); + + return { + link: function (scope, element, attr) { + var userWritingSearchTerm = false; + var minimumSearchTermLength = 3; + + scope.translationData = { + missingCharCount: minimumSearchTermLength + }; + + scope.search = {}; + scope.search.searchTerm = ""; + + scope.searchFieldBlur = function() { + userWritingSearchTerm = false; + scope.search.searchTerm = ""; + scope.translationData.missingCharCount = minimumSearchTermLength; + }; + + scope.searchFieldFocus = function() { + userWritingSearchTerm = true; + }; + + scope.showMissingCharacterText = function() { + if (!scope.search.searchTerm) { + scope.search.searchTerm = ""; + } + + if (userWritingSearchTerm) { + if (scope.search.searchTerm.length < minimumSearchTermLength) { + return true; + } + } + + return false; + }; + + scope.search = function() { + logger.trace('User is using the search term: %s', scope.search.searchTerm); + + if (scope.search.searchTerm.length < minimumSearchTermLength) { + scope.translationData.missingCharCount = minimumSearchTermLength - scope.search.searchTerm.length; + logger.trace('%s characters are missing. Search is not invoked.', scope.translationData.missingCharCount); + } + else { + scope.translationData.missingCharCount = 0; + $state.go('todo.search', + { + searchTerm: scope.search.searchTerm, + pageNumber: paginationConfig.firstPageNumber, + pageSize: paginationConfig.pageSize + }, + {reload: true, inherit: true, notify: true} + ); + } + }; + + }, + templateUrl: 'search/search-form-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('searchResultList', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + var logger = $log.getInstance('app.search.directives.searchResultList'); + + return { + link: function(scope, element, attr) { + logger.debug("Rendering search result list for search term: %s and search results: %j", scope.searchTerm, scope.searchResults); + scope.todoEntries = scope.searchResults.content; + + scope.pagination = { + currentPage: scope.searchResults.number + 1, + itemsPerPage: paginationConfig.pageSize, + totalItems: scope.searchResults.totalElements + }; + + scope.pageChanged = function(newPageNumber) { + logger.debug('Requesting a new page: %s for search term: %s with page size: %s', + newPageNumber, + scope.searchTerm, + paginationConfig.pageSize + ); + + $state.go('todo.search', + {searchTerm: scope.searchTerm, pageNumber: newPageNumber, pageSize: paginationConfig.pageSize}, + {reload: true, inherit: true, notify: true} + ); + }; + }, + templateUrl: 'search/search-result-list-directive.html', + scope: { + searchResults: '=', + searchTerm: '@' + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/search/search.services.js b/query-methods/frontend/app/module/search/search.services.js new file mode 100644 index 0000000..e007227 --- /dev/null +++ b/query-methods/frontend/app/module/search/search.services.js @@ -0,0 +1,22 @@ +'use strict'; + +angular.module('app.search.services', ['ngResource']) + .factory('TodoSearchService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/search', {}, { + 'query': {method:'GET', isArray:false} + }); + + var logger = $log.getInstance('app.search.services.TodoSearchService'); + + return { + findBySearchTerm: function(searchTerm, pageNumber, pageSize) { + logger.info('Searching todo entries with search term: %s, pageNumber: %s, and page size: %s', searchTerm, pageNumber, pageSize); + return api.query({ + page: pageNumber, + searchTerm: searchTerm, + size: pageSize, + sort: "title" + }).$promise; + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/todo/todo.controllers.js b/query-methods/frontend/app/module/todo/todo.controllers.js new file mode 100644 index 0000000..d4da7bf --- /dev/null +++ b/query-methods/frontend/app/module/todo/todo.controllers.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('app.todo.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo', { + url: '/', + abstract: true, + template: '' + }) + .state('todo.add', { + authenticate: true, + url: 'todo/add', + controller: 'AddTodoController', + templateUrl: 'todo/add-todo-view.html' + }) + .state('todo.edit', { + authenticate: true, + url: 'todo/:id/edit', + controller: 'EditTodoController', + templateUrl: 'todo/edit-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }) + .state('todo.list', { + authenticate: true, + url: '', + controller: 'TodoListController', + templateUrl: 'todo/todo-list-view.html', + resolve: { + todoEntries: ['TodoService', function(TodoService) { + return TodoService.findAll(); + }] + } + }) + .state('todo.view', { + authenticate: true, + url: 'todo/:id', + controller: 'ViewTodoController', + templateUrl: 'todo/view-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }); + } + ]) + .controller('AddTodoController', ['$log', '$scope', function($log, $scope) { + var logger = $log.getInstance('app.todo.controllers.AddTodoController'); + logger.info('Rendering add todo entry page.'); + $scope.todoEntry = {}; + }]) + .controller('EditTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.EditTodoController'); + logger.info('Rendering edit todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]) + .controller('TodoListController', ['$log', '$scope', 'todoEntries', function($log, $scope, todoEntries) { + var logger = $log.getInstance('app.todo.controllers.TodoListController'); + logger.info('Rendering todo entry list page for %s todo entries.', todoEntries.length); + $scope.todoEntries = todoEntries; + }]) + .controller('ViewTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.ViewTodoController'); + logger.info('Rendering view todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/todo/todo.directives.js b/query-methods/frontend/app/module/todo/todo.directives.js new file mode 100644 index 0000000..a2377b5 --- /dev/null +++ b/query-methods/frontend/app/module/todo/todo.directives.js @@ -0,0 +1,102 @@ +'use strict'; + +angular.module('app.todo.directives', []) + .controller('DeleteTodoController', ['$log', '$scope', '$modalInstance', '$state', 'TodoService', 'todoEntry', 'successCallback', 'errorCallback', + function($log, $scope, $modalInstance, $state, TodoService, todoEntry, successCallback, errorCallback) { + var logger = $log.getInstance('app.todo.directives.DeleteTodoController'); + + logger.info('Showing delete confirmation dialog for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + + $scope.cancel = function() { + logger.info('User clicked cancel button. Todo entry is not deleted.'); + $modalInstance.dismiss('cancel'); + }; + + $scope.delete = function() { + logger.info('User clicked delete button. Todo entry is deleted.'); + $modalInstance.close(); + TodoService.delete(todoEntry, successCallback, errorCallback); + }; + }]) + .directive('deleteTodoEntryButton', ['$modal', '$state', 'NotificationService', function($modal, $state, NotificationService) { + return { + link: function (scope, element, attr) { + scope.onSuccess = function() { + NotificationService.flashMessage('todo.notifications.delete.success', 'success'); + $state.go('todo.list'); + }; + + scope.onError = function() { + NotificationService.flashMessage('todo.notifications.delete.error', 'error'); + }; + + scope.showDeleteConfirmationDialog = function() { + $modal.open({ + templateUrl: 'todo/delete-todo-modal.html', + controller: 'DeleteTodoController', + resolve: { + errorCallback: function() { + return scope.onError; + }, + successCallback: function() { + return scope.onSuccess; + }, + todoEntry: function () { + return scope.todoEntry; + } + } + }); + }; + }, + template: '', + scope: { + todoEntry: '=' + } + }; + }]) + .directive('todoEntryForm', ['$log', '$state', 'NotificationService', 'TodoService', function($log, $state, NotificationService, TodoService) { + var logger = $log.getInstance('app.todo.directives.todoEntryForm'); + + return { + link: function (scope, element, attr) { + scope.saveTodoEntry = function() { + logger.info('Saving todo entry: %j', scope.todoEntry); + + var onSuccess = function(saved) { + NotificationService.flashMessage(scope.successMessageKey, 'success'); + $state.go('todo.view', {id: saved.id}); + }; + + var onError = function() { + NotificationService.flashMessage(scope.errorMessageKey, 'errors'); + }; + + if (scope.formType === 'add') { + TodoService.add(scope.todoEntry, onSuccess, onError); + } + else if (scope.formType === 'edit') { + TodoService.update(scope.todoEntry, onSuccess, onError); + } + else { + logger.error('Unknown form type: %s', scope.formType); + } + }; + }, + templateUrl: 'todo/todo-form-directive.html', + scope: { + errorMessageKey: '@', + formType: '@', + todoEntry: '=', + successMessageKey: '@' + } + }; + }]) + .directive('todoEntryList', [function() { + return { + templateUrl: 'todo/todo-list-directive.html', + scope: { + todoEntries: '=' + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/module/todo/todo.services.js b/query-methods/frontend/app/module/todo/todo.services.js new file mode 100644 index 0000000..f49f622 --- /dev/null +++ b/query-methods/frontend/app/module/todo/todo.services.js @@ -0,0 +1,61 @@ +'use strict'; + +angular.module('app.todo.services', ['ngResource']) + .factory('TodoService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/:id', {"id": "@id"}, { + get: {method: 'GET'}, + save: {method: 'POST'}, + update: {method: 'PUT'}, + query: {method: 'GET', params: {}, isArray: true} + }); + + var logger = $log.getInstance('app.todo.services.TodoService'); + + return { + add: function(todo, successCallback, errorCallback) { + logger.info('Adding new todo entry: %j', todo); + return api.save(todo, + function(added) { + logger.info('Added a new todo entry: %j', added); + successCallback(added); + }, + function(error) { + logger.error('Adding a todo entry failed because of an error: %j', error); + errorCallback(error); + }); + }, + delete: function(todo, successCallback, errorCallback) { + logger.info('Deleting todo entry: %j', todo); + return api.delete(todo, + function(deleted) { + logger.info('Deleted todo entry: %j', deleted); + successCallback(deleted); + }, + function(error) { + logger.error('Deleting the todo entry failed because of an error: %j', error); + errorCallback(error); + } + ); + }, + findAll: function() { + logger.info('Finding all todo entries.'); + return api.query(); + }, + findById: function(id) { + logger.info('Finding todo entry by id: %s', id); + return api.get({id: id}).$promise; + }, + update: function(todo, successCallback, errorCallback) { + logger.info('Updating todo entry: %j', todo); + return api.update(todo, + function(updated) { + logger.info('Updated the information of the todo entry: %j', updated); + successCallback(updated); + }, + function(error) { + logger.error('Updating the information of the todo entry failed because of an error: %j', error); + errorCallback(error); + }); + } + }; + }]); \ No newline at end of file diff --git a/query-methods/frontend/app/styles/app.less b/query-methods/frontend/app/styles/app.less new file mode 100644 index 0000000..4e70998 --- /dev/null +++ b/query-methods/frontend/app/styles/app.less @@ -0,0 +1,74 @@ +[ng-cloak] { + display: none; +} + +@import "https://github.com/bower_components/bootstrap/less/bootstrap.less"; + +// Red asterisk for required labels +label.required:before{ + content:"* "; + color:red; +} + +// styles for custom input validation +input.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +input.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +textarea.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +small.ng-error { + color: #a94442; +} + +a:hover { + cursor: pointer; +} + +.striped-list { + > .row:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + +.striped-list .row { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; +} + +.action-buttons { + text-align: right; +} diff --git a/query-methods/frontend/bower.json b/query-methods/frontend/bower.json new file mode 100644 index 0000000..8390db6 --- /dev/null +++ b/query-methods/frontend/bower.json @@ -0,0 +1,40 @@ +{ + "name": "Spring Data JPA Tutorial - Query Methods", + "version": "0.0.1", + "main": "_public/frontend/js/app.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "console-polyfill": "~0.2.1", + "lodash": "~3.8.0", + "moment": "2.10.6", + "jquery": "2.1.0", + "bootstrap": "~3.3.4", + "angular": "~1.3.15", + "angular-http-auth": "1.2.2", + "angular-i18n": "~1.3.15", + "angular-moment": "0.10.1", + "angular-logger": "1.0.1", + "angular-sanitize": "~1.3.15", + "angular-resource": "~1.3.15", + "angular-cookies": "~1.3.15", + "angular-loader": "~1.3.15", + "angular-mocks": "~1.3.15", + "angular-translate": "~2.7.0", + "angular-translate-storage-local": "~2.7.0", + "angular-translate-loader-static-files": "~2.7.0", + "angular-translate-handler-log": "~2.7.0", + "angular-ui-utils": "~0.2.3", + "angular-ui-router": "~0.2.15", + "angular-bootstrap": "~0.13.0", + "angular-growl-v2": "0.7.3", + "angular-utils-pagination": "0.8.2", + "es5-shim": "~4.1.1", + "json3": "~3.3.2", + "script.js": "~2.5.7", + "sprintf": "1.0.3" + } +} \ No newline at end of file diff --git a/query-methods/frontend/build.config.js b/query-methods/frontend/build.config.js new file mode 100644 index 0000000..963b2f3 --- /dev/null +++ b/query-methods/frontend/build.config.js @@ -0,0 +1,77 @@ +'use strict'; + +var path = require('path'); + +var targetBase = './build/'; + +module.exports = { + //Configures the directories in which the files created by Gulp are copied. + target: { + js: targetBase + '/js', + lib: path.join(targetBase, 'js', 'lib'), + css: path.join(targetBase, 'css'), + partials: path.join(targetBase, 'partials'), + assets: targetBase + }, + + //Configures the location of the used libraries and frameworks. + vendorFiles: { + code: [ + './bower_components/console-polyfill/index.js', + './bower_components/lodash/dist/lodash.min.js', + './bower_components/jquery/dist/jquery.min.js', + './bower_components/angular/angular.js', + './bower_components/moment/min/moment-with-locales.min.js', + './bower_components/sprintf/dist/sprintf.min.js', + './bower_components/angular-http-auth/src/http-auth-interceptor.js', + './bower_components/angular-i18n/angular-locale_fi-fi.js', + './bower_components/angular-cookies/angular-cookies.min.js', + './bower_components/angular-moment/angular-moment.min.js', + './bower_components/angular-logger/dist/angular-logger.min.js', + './bower_components/angular-resource/angular-resource.min.js', + './bower_components/angular-sanitize/angular-sanitize.min.js', + './bower_components/angular-translate/angular-translate.min.js', + './bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', + './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js', + './bower_components/angular-translate-storage-local/angular-translate-storage-local.min.js', + './bower_components/angular-translate-handler-log/angular-translate-handler-log.min.js', + './bower_components/angular-ui-router/release/angular-ui-router.min.js', + './bower_components/angular-ui-utils/ui-utils.min.js', + './bower_components/angular-ui-utils/ui-utils-ieshiv.min.js', + './bower_components/angular-utils-pagination/dirPagination.js', + './bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', + './bower_components/angular-growl-v2/build/angular-growl.min.js', + './vendor/spring-security-csrf-token-interceptor/src/spring-security-csrf-token-interceptor.js' + ] + }, + + //Configures the location of our application's files. + appFiles: { + //Configures the location of the Javascript files. + code: [ + "./app/**/*.js" + ], + //Configures the location of the LESS files. + styleBase: "./app/styles/", + style: [ + "./bower_components/angular-growl-v2/build/angular-growl.min.css", + "./app/styles/app.less" + ], + //Configures the location of the view templates. + partials: [ + "./app/assets/partials/**/*.html" + ], + //Configures the location of static assets such as images, fonts, and localization files. + assetsBase: './app/assets/', + assets: [ + './app/assets/**' + ], + //Configures the location of shims (libraries that bring new APIs to older browsers) + shim: [ + './bower_components/angular-loader/angular-loader.min.js', + './bower_components/script.js/dist/script.min.js', + './bower_components/es5-shim/es5-shim.min.js', + './bower_components/json3/lib/json3.min.js' + ] + } +}; \ No newline at end of file diff --git a/query-methods/frontend/gulpfile.js b/query-methods/frontend/gulpfile.js new file mode 100644 index 0000000..40f10f7 --- /dev/null +++ b/query-methods/frontend/gulpfile.js @@ -0,0 +1,124 @@ +var gulp = require("gulp"); +var plugins = require('gulp-load-plugins')(); +var config = require('./build.config.js'); + +//Analyzes the Javascript files of our application by using JSHint and reports the found problems. +gulp.task('jshint', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.changed(config.target.js)) + .pipe(plugins.jshint('.jshintrc')) + .pipe(plugins.jshint.reporter('jshint-stylish')); +}); + +//Processes the Javascript files of our application. +gulp.task('appCode', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.sourcemaps.init()) + //Combines the Javascript files into a single Javascript file + .pipe(plugins.concat('app.min.js')) + //Minifies the created Javascript file + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.sourcemaps.write()) + //Copies the minified Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file. + .pipe(plugins.size({title: 'application'})) +}); + +//Processes the HTML templates of our application. +gulp.task('appPartials', function () { + return gulp.src(config.appFiles.partials) + .pipe(plugins.changed(config.target.js)) + //Minifies the HTML files + .pipe(plugins.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + //Loads the HTML templates into AngularJS $templateCache + .pipe(plugins.angularTemplatecache('partials.js', { + standalone: true + })) + //Copy the created Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of created Javascript file + .pipe(plugins.size({showFiles: true})) +}); + +//Processes the LESS files of our application. +gulp.task('appLess', function () { + return gulp.src(config.appFiles.style) + //Creates the final CSS file + .pipe(plugins.less({ + paths: [config.appFiles.styleBase] + })) + .pipe(plugins.concat('app.css')) + //Minifies the created CSS file + .pipe(plugins.minifyCss()) + //Copies the CSS File into the target directory + .pipe(gulp.dest(config.target.css)) + //Reports the size of the final CSS file. + .pipe(plugins.size({ title: 'css' })) +}); + +gulp.task('appAssets', function () { + return gulp.src(config.appFiles.assets, {base: config.appFiles.assetsBase}) + .pipe(gulp.dest(config.target.assets)) +}); + +//Minimizes the shims used by our application and copies them to the target directory. +gulp.task('appShim', function () { + return gulp.src(config.appFiles.shim) + .pipe(plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + })) + .pipe(gulp.dest(config.target.lib)); +}); + +//Processes the Javascript files of the libraries and frameworks that are used in our application +gulp.task('vendorCode', function () { + return gulp.src(config.vendorFiles.code) + //Combine the Javascript files into a single Javascript file + .pipe(plugins.concat('vendor.min.js')) + //Skips minification of files that are already minified. + .pipe(plugins.if('*.min.js', plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + }))) + //Minifies Javascript files that are not minified. + .pipe(plugins.if('vendor/**/*.js', plugins.uglify({ + mangle: false, + compress: true + }))) + //Copies the created file to the target directory. + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file + .pipe(plugins.size({title: 'vendor'})) +}); + +//Analyzes our Javascript files by using JSHint and invokes the build when the watched files are changed +gulp.task('watch', ['jshint', 'build'], function () { + gulp.watch(config.appFiles.partials, ['appPartials']); + gulp.watch(config.appFiles.code, ['appCode', 'jshint']); + gulp.watch(config.appFiles.style, ['appLess']); + gulp.watch(config.appFiles.assets, ['appAssets']); + gulp.watch(config.vendorFiles.code, ['vendorCode']); +}); + +//Configures the tasks of our build +gulp.task('build', [ + 'appLess', + 'appShim', + 'appAssets', + 'appPartials', + 'appCode', + 'vendorCode' +]); + +//Runs the watch task if no task is specified when gulp is run +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/query-methods/frontend/package.json b/query-methods/frontend/package.json new file mode 100644 index 0000000..55dfccd --- /dev/null +++ b/query-methods/frontend/package.json @@ -0,0 +1,38 @@ +{ + "author": "Petri Kainulainen", + "name": "spring-data-jpa-tutorial-query-methods", + "description": "Angular frontend for a Spring Data JPA example.", + "version": "1.0.0", + "homepage": "", + "repository": { + "type": "git", + "url": "" + }, + "dependencies": { + "bower": "~1.4.1", + "gulp": "~3.8.11", + "gulp-angular-templatecache": "~1.6.0", + "gulp-changed": "~1.2.1", + "gulp-concat": "~2.5.2", + "gulp-if": "~1.2.5", + "gulp-insert": "^0.4.0", + "gulp-jshint": "~1.10.0", + "gulp-less": "~3.0.3", + "gulp-load-plugins": "~0.10.0", + "gulp-minify-css": "~1.1.1", + "gulp-minify-html": "~1.0.2", + "gulp-rename": "~1.2.2", + "gulp-size": "~1.2.1", + "gulp-sourcemaps": "~1.5.2", + "gulp-uglify": "~1.2.0", + "jshint-stylish": "~1.0.2" + }, + "engines": { + "node": ">=0.12.0" + } +} + + + + + diff --git a/query-methods/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js b/query-methods/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js new file mode 100644 index 0000000..84318da --- /dev/null +++ b/query-methods/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("spring-security-csrf-token-interceptor",[]).factory("csrfInterceptor",["$injector","$q",function($injector){var $q=$injector.get("$q"),csrf=$injector.get("csrf"),csrfService=csrf.init();return{request:function(config){return csrfService.settings.httpTypes.indexOf(config.method.toUpperCase())>-1&&(config.headers[csrfService.settings.csrfTokenHeader]=csrfService.token),config||$q.when(config)},responseError:function(response){var $http,newToken=response.headers(csrfService.settings.csrfTokenHeader);return 403===response.status&&csrfService.numRetries -1) { + config.headers[csrfService.settings.csrfTokenHeader] = csrfService.token; + } + return config || $q.when(config); + }, + responseError: function(response) { + var $http, + newToken = response.headers(csrfService.settings.csrfTokenHeader); + + if (response.status === 403 && csrfService.numRetries < csrfService.settings.maxRetries) { + csrfService.getTokenData(); + $http = $injector.get('$http'); + csrfService.numRetries = csrfService.numRetries + 1; + return $http(response.config); + } else if (newToken) { + // update the csrf token in-case of response errors other than 403 + csrfService.token = newToken; + } + // Fix for interceptor causing failing requests + return $q.reject(response); + }, + response: function(response) { + // reset number of retries on a successful response + csrfService.numRetries = 0; + return response; + } + }; + } + ]).factory('csrfService', [ + + function() { + var defaults = { + url: '/', // the URL to which the CSRF call has to be made to get the token + csrfHttpType: 'head', // the HTTP method type which is used for making the CSRF token call + maxRetries: 5, // number of retires allowed for forbidden requests + csrfTokenHeader: 'X-CSRF-TOKEN', + httpTypes: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] // default allowed HTTP types + }; + return { + inited: false, + settings: null, + numRetries: 0, + token: '', + init: function(options) { + this.settings = angular.extend({}, defaults, options); + this.getTokenData(); + console.log(this.settings, this.defaults, options); + }, + getTokenData: function() { + var xhr = new XMLHttpRequest(); + xhr.open(this.settings.csrfHttpType, this.settings.url, false); + xhr.send(); + + this.token = xhr.getResponseHeader(this.settings.csrfTokenHeader); + this.inited = true; + } + }; + + } + ]).provider('csrf', [ + + function() { + var CsrfModel = function CsrfModel(options) { + return { + options: options, + csrfService: null + }; + }; + + return { + $get: ['csrfService', + function(csrfService) { + var self = this; + return { + init: function() { + self.model = new CsrfModel(self.options); + self.model.csrfService = csrfService; + self.model.csrfService.init(self.model.options); + return self.model.csrfService; + } + }; + } + ], + + model: null, + + options: {}, + + config: function(options) { + this.options = options; + } + }; + } + ]).config(['$httpProvider', + function($httpProvider) { + $httpProvider.interceptors.push('csrfInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/query-methods/pom.xml b/query-methods/pom.xml index d599f03..f72bfe7 100644 --- a/query-methods/pom.xml +++ b/query-methods/pom.xml @@ -16,7 +16,7 @@ io.spring.platform platform-bom - 1.1.0.RELEASE + 1.1.2.RELEASE pom import @@ -28,6 +28,7 @@ UTF-8 true false + 4.0.1.RELEASE @@ -103,6 +104,10 @@ javax.servlet-api provided + + javax.servlet + jstl + org.springframework spring-webmvc @@ -124,6 +129,22 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + @@ -151,7 +172,7 @@ org.assertj assertj-core - 1.7.0 + 3.1.0 test @@ -164,11 +185,23 @@ mockito-core test + + info.solidsoft.mockito + mockito-java8 + 0.3.0 + test + org.springframework spring-test test + + org.springframework.security + spring-security-test + ${spring.security.version} + test + com.jayway.jsonpath json-path @@ -183,13 +216,13 @@ com.github.springtestdbunit spring-test-dbunit - 1.1.0 + 1.2.1 test org.dbunit dbunit - 2.5.0 + 2.5.1 test @@ -200,7 +233,7 @@ - jpa-query-methods + ROOT org.codehaus.mojo @@ -250,7 +283,15 @@ maven-war-plugin 2.5 + ROOT false + + + frontend/build + / + false + + @@ -289,8 +330,9 @@ org.eclipse.jetty jetty-maven-plugin - 9.2.5.v20141112 + 9.2.10.v20150310 + 0 stop 9999 @@ -299,8 +341,35 @@ application + + ${project.basedir}/target/ROOT.war + / + + ${project.basedir}/src/main/webapp + ${project.basedir}/frontend/build + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + + run + + + + \ No newline at end of file diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java new file mode 100644 index 0000000..3e294d5 --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * This class contains the constants that are used in our integration tests, DbUnit datasets, + * and the localization file. + * + * @author Petri Kainulainen + */ +public final class TodoConstants { + + public static class TodoEntries { + + public static class First { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "description"; + public static final Long ID = 1L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "title"; + } + + public static class Second { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "tiscription"; + public static final Long ID = 2L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "First"; + + } + } + + public static final String SEARCH_TERM_DESCRIPTION_MATCHES = "esC"; + public static final String SEARCH_TERM_NO_MATCH = "NO MATCH"; + public static final String SEARCH_TERM_TITLE_MATCHES = "It"; + + public static final String UPDATED_DESCRIPTION = "updatedDescription"; + public static final String UPDATED_TITLE = "updatedTitle"; + + public static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; + public static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; + public static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; + public static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; + + /** + * Prevents instantiation + */ + private TodoConstants() {} +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java new file mode 100644 index 0000000..77cdb31 --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * @author Petri Kainulainen + */ +public enum Users { + + USER("user", "password", "ROLE_USER"); + + private String password; + private String role; + private String username; + + Users(String username, String password, String role) { + this.password = password; + this.role = role; + this.username = username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } + + public String getUsername() { + return username; + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITMethodNameTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITMethodNameTest.java new file mode 100644 index 0000000..b9a6272 --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITMethodNameTest.java @@ -0,0 +1,236 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.assertj.core.api.StrictAssertions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITMethodNameTest { + + private static final int PAGE_NUMBER_ONE = 0; + private static final int PAGE_NUMBER_TWO = 1; + private static final int PAGE_SIZE_ONE = 1; + private static final int PAGE_SIZE_TWO = 2; + + private static final String SEARCH_TERM = "tIo"; + + @Autowired + private TodoRepository repository; + + private Sort orderByTitleAsc; + + @Before + public void orderByTitleAsc() { + orderByTitleAsc = new Sort(Sort.Direction.ASC, "title"); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_DescriptionOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, + TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, + createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO) + ); + + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_DescriptionOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, + TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, + pageRequest + ); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_NoMatch_ShouldReturnPageWithTotalElementCountZero() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_NO_MATCH, + TodoConstants.SEARCH_TERM_NO_MATCH, + pageRequest + ); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_NoMatch_ShouldReturnEmptyPage() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_NO_MATCH, + TodoConstants.SEARCH_TERM_NO_MATCH, + pageRequest + ); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_TitleOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_TITLE_MATCHES, + TodoConstants.SEARCH_TERM_TITLE_MATCHES, + pageRequest + ); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_TitleOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(TodoConstants.SEARCH_TERM_TITLE_MATCHES, + TodoConstants.SEARCH_TERM_TITLE_MATCHES, + pageRequest + ); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCase_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageWithTotalElementCountTwo() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(SEARCH_TERM, + SEARCH_TERM, + pageRequest + ); + assertThat(searchResultPage.getTotalElements()).isEqualTo(2); + } + + @Test + public void findFirstPageByDescriptionContainsOrTitleContainsAllIgnoreCaseWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheSecondTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(SEARCH_TERM, + SEARCH_TERM, + pageRequest + ); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findSecondPageByDescriptionContainsOrTitleContainsAllIgnoreCaseWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheFirstTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_TWO, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(SEARCH_TERM, + SEARCH_TERM, + pageRequest + ); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findFirstPageByDescriptionContainsOrTitleContainsAllIgnoreCaseWithPageSizeTwo_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTwoTodoEntries() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCase(SEARCH_TERM, + SEARCH_TERM, + pageRequest + ); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(2); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = searchResultPage.getContent().get(1); + StrictAssertions.assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + private Pageable createPageRequest(int pageNumber, int pageSize) { + return new PageRequest(pageNumber, pageSize, orderByTitleAsc); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, + TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES + ); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc_NoMatches_ShouldReturnEmptyList() { + List todoEntries = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc(TodoConstants.SEARCH_TERM_NO_MATCH, + TodoConstants.SEARCH_TERM_NO_MATCH + ); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc(TodoConstants.SEARCH_TERM_TITLE_MATCHES, + TodoConstants.SEARCH_TERM_TITLE_MATCHES + ); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc(SEARCH_TERM, + SEARCH_TERM + ); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITNamedQueryTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITNamedQueryTest.java new file mode 100644 index 0000000..8eba9c1 --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITNamedQueryTest.java @@ -0,0 +1,458 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITNamedQueryTest { + + private static final int PAGE_NUMBER_ONE = 0; + private static final int PAGE_NUMBER_TWO = 1; + private static final int PAGE_SIZE_ONE = 1; + private static final int PAGE_SIZE_TWO = 2; + + private static final String SEARCH_TERM = "tIo"; + + @Autowired + private TodoRepository repository; + + + @Test + public void findBySearchTermNamed_DescriptionOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamed_DescriptionOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamed_NoMatch_ShouldReturnPageWithTotalElementCountZero() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + + @Test + public void findBySearchTermNamed_NoMatch_ShouldReturnEmptyPage() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void findBySearchTermNamed_TitleOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamed_TitleOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamed_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageWithTotalElementCountTwo() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(2); + } + + @Test + public void findFirstPageBySearchTermNamedWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheSecondTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamed(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findSecondPageBySearchTermNamedWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheFirstTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_TWO, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamed(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findFirstPageBySearchTermNamedWithPageSizeTwo_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTwoTodoEntries() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamed(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(2); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = searchResultPage.getContent().get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + private Pageable createPageRequest(int pageNumber, int pageSize) { + return new PageRequest(pageNumber, pageSize); + } + + @Test + public void findBySearchTermNamedNative_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNative(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNative_NoMatches_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTermNamedNative(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTermNamedNativeSorted_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNative(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNative_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTermNamedNative(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedFile_DescriptionOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamedFile_DescriptionOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedFile_NoMatch_ShouldReturnPageWithTotalElementCountZero() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + + @Test + public void findBySearchTermNamedFile_NoMatch_ShouldReturnEmptyPage() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void findBySearchTermNamedFile_TitleOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamedFile_TitleOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedFile_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageWithTotalElementCountTwo() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(2); + } + + @Test + public void findFirstPageBySearchTermNamedFileWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheSecondTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamedFile(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findSecondPageBySearchTermNamedFileWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheFirstTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_TWO, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamedFile(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findFirstPageBySearchTermNamedFileWithPageSizeTwo_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTwoTodoEntries() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedFile(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(2); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = searchResultPage.getContent().get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeFile_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNativeFile(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeFile_NoMatches_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTermNamedNativeFile(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTermNamedNativeFile_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNativeFile(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeFile_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTermNamedNativeFile(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedOrmXml_DescriptionOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamedOrmXml_DescriptionOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedOrmXml_NoMatch_ShouldReturnPageWithTotalElementCountZero() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + + @Test + public void findBySearchTermNamedOrmXml_NoMatch_ShouldReturnEmptyPage() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void findBySearchTermNamedOrmXml_TitleOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTermNamedOrmXml_TitleOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedOrmXml_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageWithTotalElementCountTwo() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(2); + } + + @Test + public void findFirstPageBySearchTermNamedOrmXmlWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheSecondTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findSecondPageBySearchTermNamedOrmXmlWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheFirstTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_TWO, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findFirstPageBySearchTermNamedOrmXmlWithPageSizeTwo_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTwoTodoEntries() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTermNamedOrmXml(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(2); + + Todo first = searchResultPage.getContent().get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = searchResultPage.getContent().get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeOrmXml_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNativeOrmXml(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeOrmXml_NoMatches_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTermNamedNativeOrmXml(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTermNamedNativeOrmXml_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNamedNativeOrmXml(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNamedNativeOrmXml_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTermNamedNativeOrmXml(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITQueryAnnotationTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITQueryAnnotationTest.java new file mode 100644 index 0000000..c18aa73 --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITQueryAnnotationTest.java @@ -0,0 +1,235 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.assertj.core.api.StrictAssertions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITQueryAnnotationTest { + + private static final int PAGE_NUMBER_ONE = 0; + private static final int PAGE_NUMBER_TWO = 1; + private static final int PAGE_SIZE_ONE = 1; + private static final int PAGE_SIZE_TWO = 2; + + private static final String SEARCH_TERM = "tIo"; + + @Autowired + private TodoRepository repository; + + private Sort orderByTitleAsc; + + @Before + public void orderByTitleAsc() { + orderByTitleAsc = new Sort(Sort.Direction.ASC, "title"); + } + + @Test + public void findBySearchTerm_DescriptionOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTerm_DescriptionOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnPageWithTotalElementCountZero() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnEmptyPage() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_NO_MATCH, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void findBySearchTerm_TitleOfOneTodoEntryMatches_ShouldReturnPageWithTotalElementCountOne() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + + @Test + public void findBySearchTerm_TitleOfFirstTodoEntryMatches_ShouldReturnPageThatHasOneTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(TodoConstants.SEARCH_TERM_TITLE_MATCHES, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo todoEntry = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTerm_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageWithTotalElementCountTwo() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(2); + } + + @Test + public void findFirstPageBySearchTermWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheSecondTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findSecondPageBySearchTermWithPageSizeOne_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTheFirstTodoEntry() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_TWO, PAGE_SIZE_ONE); + + Page searchResultPage = repository.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findFirstPageBySearchTermWithPageSizeTwo_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnPageThatHasTwoTodoEntries() { + Pageable pageRequest = createPageRequest(PAGE_NUMBER_ONE, PAGE_SIZE_TWO); + + Page searchResultPage = repository.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(2); + + Todo first = searchResultPage.getContent().get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = searchResultPage.getContent().get(1); + StrictAssertions.assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + private Pageable createPageRequest(int pageNumber, int pageSize) { + return new PageRequest(pageNumber, pageSize, orderByTitleAsc); + } + + @Test + public void findBySearchTermSortedInQuery_DescriptionOfFirstTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermSortedInQuery(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermSortedInQuery_NoMatch_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTermSortedInQuery(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTermSortedInQuery_TitleOfFirstTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermSortedInQuery(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermSortedInQuery_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTermSortedInQuery(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + StrictAssertions.assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNative_DescriptionOfFirstTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNative(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNative_NoMatch_ShouldReturnEmptyList() { + List todoEntries = repository.findBySearchTermNative(TodoConstants.SEARCH_TERM_NO_MATCH); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTermNative_TitleOfFirstTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + List todoEntries = repository.findBySearchTermNative(TodoConstants.SEARCH_TERM_TITLE_MATCHES); + assertThat(todoEntries).hasSize(1); + + Todo todoEntry = todoEntries.get(0); + StrictAssertions.assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTermNative_TwoTodoEntriesMatchesWithSearchTerm_ShouldReturnSortedListThatHasTwoTodoEntries() { + List todoEntries = repository.findBySearchTermNative(SEARCH_TERM); + assertThat(todoEntries).hasSize(2); + + Todo first = todoEntries.get(0); + StrictAssertions.assertThat(first.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + + Todo second = todoEntries.get(1); + StrictAssertions.assertThat(second.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java index 0b39bcd..af912d1 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java @@ -1,12 +1,9 @@ package net.petrikainulainen.springdata.jpa.web; -import com.github.springtestdbunit.dataset.AbstractDataSetLoader; +import com.github.springtestdbunit.dataset.FlatXmlDataSetLoader; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.ReplacementDataSet; -import org.dbunit.dataset.xml.FlatXmlDataSet; -import org.dbunit.dataset.xml.FlatXmlDataSetBuilder; import org.springframework.core.io.Resource; -import java.io.InputStream; /** * This class is a custom DbUnit data set loader that support flat XML data sets. This data set loader * adds support for the extra features: @@ -16,17 +13,13 @@ * * @author Petri Kainulainen */ -public class ColumnSensingReplacementDataSetLoader extends AbstractDataSetLoader { +public class ColumnSensingReplacementDataSetLoader extends FlatXmlDataSetLoader { @Override protected IDataSet createDataSet(Resource resource) throws Exception { - FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder(); - builder.setColumnSensing(true); - try (InputStream inputStream = resource.getInputStream()) { - return createReplacementDataSet(builder.build(inputStream)); - } + return createReplacementDataSet(super.createDataSet(resource)); } - private ReplacementDataSet createReplacementDataSet(FlatXmlDataSet dataSet) { + private ReplacementDataSet createReplacementDataSet(IDataSet dataSet) { ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet); replacementDataSet.addReplacementObject("[null]", null); return replacementDataSet; diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java index 27e78ba..06b9c1c 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java @@ -5,6 +5,9 @@ import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; import net.petrikainulainen.springdata.jpa.config.Profiles; import net.petrikainulainen.springdata.jpa.todo.TestUtil; @@ -14,12 +17,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -31,7 +35,8 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -45,9 +50,10 @@ @ContextConfiguration(classes = {ExampleApplicationContext.class}) @DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, - DbUnitTestExecutionListener.class}) + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) @WebAppConfiguration @DatabaseSetup("no-todo-entries.xml") public class ITCreateTest { @@ -62,27 +68,44 @@ public void setUp() throws SQLException { DbTestUtil.resetAutoIncrementColumns(webAppContext, "todos"); mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) .build(); } @Test - public void create_EmptyTodoEntry_ShouldReturnResponseStatusBadRequest() throws Exception { + public void create_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { TodoDTO emptyTodoEntry = new TodoDTO(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) ) .andExpect(status().isBadRequest()); } @Test - public void create_EmptyTodoEntry_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { TodoDTO emptyTodoEntry = new TodoDTO(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) @@ -93,17 +116,20 @@ public void create_EmptyTodoEntry_ShouldReturnValidationErrorAboutMissingTitleAs @Test @ExpectedDatabase("no-todo-entries.xml") - public void create_EmptyTodoEntry_ShouldNotSaveTodoEntry() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { TodoDTO emptyTodoEntry = new TodoDTO(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) ); } @Test - public void create_TooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); @@ -115,12 +141,14 @@ public void create_TooLongTitleAndDescription_ShouldReturnResponseStatusBadReque mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ) .andExpect(status().isBadRequest()); } @Test - public void create_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); @@ -132,6 +160,7 @@ public void create_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutT mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) @@ -148,7 +177,8 @@ public void create_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutT @Test @ExpectedDatabase("no-todo-entries.xml") - public void create_TooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); @@ -160,53 +190,62 @@ public void create_TooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Ex mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ); } @Test - public void create_ValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(TodoConstants.DESCRIPTION) - .title(TodoConstants.TITLE) + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) .build(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ) .andExpect(status().isCreated()); } @Test - public void create_ValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(TodoConstants.DESCRIPTION) - .title(TodoConstants.TITLE) + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) .build(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", isA(String.class))) - .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) - .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", isA(String.class))) - .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + .andExpect(jsonPath("$.createdByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.creationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); } @Test @ExpectedDatabase(value = "create-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) - public void create_ValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(TodoConstants.DESCRIPTION) - .title(TodoConstants.TITLE) + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) .build(); mockMvc.perform(post("/api/todo") .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) ); } } diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java index 1f2fb7c..ed8aca6 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java @@ -4,18 +4,20 @@ import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; import net.petrikainulainen.springdata.jpa.config.Profiles; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -25,6 +27,8 @@ import java.sql.SQLException; import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -37,10 +41,10 @@ @ActiveProfiles(Profiles.INTEGRATION_TEST) @ContextConfiguration(classes = {ExampleApplicationContext.class}) @DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) -@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class, +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class, - DbUnitTestExecutionListener.class }) + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) @WebAppConfiguration public class ITDeleteTest { @@ -52,20 +56,36 @@ public class ITDeleteTest { @Before public void setUp() throws SQLException { mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) .build(); } @Test @DatabaseSetup("no-todo-entries.xml") - public void delete_TodoEntryNotFound_ShouldReturnResponseStatusBadRequest() throws Exception { - mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID)) + public void delete_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) .andExpect(status().isNotFound()); } @Test @DatabaseSetup("no-todo-entries.xml") - public void delete_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { - mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID)) + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); @@ -74,28 +94,39 @@ public void delete_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exc @Test @DatabaseSetup("no-todo-entries.xml") @ExpectedDatabase("no-todo-entries.xml") - public void delete_TodoEntryNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { - mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID)) + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); } @Test - @DatabaseSetup("todo-entries.xml") - public void delete_TodoEntryFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { - mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID)) - .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) - .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) - .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); } @Test - @DatabaseSetup("todo-entries.xml") + @DatabaseSetup("one-todo-entry.xml") @ExpectedDatabase("delete-todo-entry-expected.xml") - public void delete_TodoEntryFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { - mockMvc.perform(delete("/api/todo/{id}", TodoConstants.ID)); + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ); } } diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java index bd6cf99..5781ca7 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java @@ -3,18 +3,20 @@ import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; import net.petrikainulainen.springdata.jpa.config.Profiles; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -23,6 +25,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; 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.jsonPath; @@ -35,10 +38,10 @@ @ActiveProfiles(Profiles.INTEGRATION_TEST) @ContextConfiguration(classes = {ExampleApplicationContext.class}) @DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) -@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class, +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class, - DbUnitTestExecutionListener.class }) + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) @WebAppConfiguration public class ITFindAllTest { @@ -50,33 +53,45 @@ public class ITFindAllTest { @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) .build(); } @Test - public void findAll_ShouldReturnResponseStatusOk() throws Exception { + public void findAll_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findAll_AsUser_ShouldReturnResponseStatusOk() throws Exception { mockMvc.perform(get("/api/todo")) .andExpect(status().isOk()); } @Test @DatabaseSetup("no-todo-entries.xml") - public void findAll_NoTodoEntriesFound_ShouldReturnEmptyListAsJson() throws Exception { + @WithUserDetails("user") + public void findAll_AsUser_WhenTodoEntriesAreNotFound_ShouldReturnEmptyListAsJson() throws Exception { mockMvc.perform(get("/api/todo")) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$", hasSize(0))); } @Test - @DatabaseSetup("todo-entries.xml") - public void findAll_OneTodoEntryFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenOneTodoEntryIsFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { mockMvc.perform(get("/api/todo")) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.CREATION_TIME))) - .andExpect(jsonPath("$[0].description", is(TodoConstants.DESCRIPTION))) - .andExpect(jsonPath("$[0].id", is(TodoConstants.ID.intValue()))) - .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.MODIFICATION_TIME))) - .andExpect(jsonPath("$[0].title", is(TodoConstants.TITLE))); + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TodoEntries.First.TITLE))); } } diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java index 849c3f4..393d146 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java @@ -3,18 +3,20 @@ import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; import net.petrikainulainen.springdata.jpa.config.Profiles; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -22,6 +24,7 @@ import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; 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.jsonPath; @@ -35,9 +38,9 @@ @ContextConfiguration(classes = {ExampleApplicationContext.class}) @DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, - DbUnitTestExecutionListener.class }) + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) @WebAppConfiguration public class ITFindByIdTest { @@ -49,20 +52,30 @@ public class ITFindByIdTest { @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) .build(); } @Test @DatabaseSetup("no-todo-entries.xml") - public void findById_TodoEntryNotFound_ShouldReturnResponseStatusNotFound() throws Exception { - mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + public void findById_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) .andExpect(status().isNotFound()); } @Test @DatabaseSetup("no-todo-entries.xml") - public void findById_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { - mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); @@ -70,21 +83,25 @@ public void findById_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws E } @Test - @DatabaseSetup("todo-entries.xml") - public void findById_TodoEntryFound_ShouldReturnResponseStatusOk() throws Exception { - mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) .andExpect(status().isOk()); } @Test - @DatabaseSetup("todo-entries.xml") - public void findById_TodoEntryFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { - mockMvc.perform(get("/api/todo/{id}", TodoConstants.ID)) + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) - .andExpect(jsonPath("$.description", is(TodoConstants.DESCRIPTION))) - .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(TodoConstants.MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(TodoConstants.TITLE))); + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); } } diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java new file mode 100644 index 0000000..b08ad3c --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java @@ -0,0 +1,317 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("two-todo-entries.xml") +public class ITFindBySearchTermTest { + + private static final int FIRST_PAGE = 0; + private static final String FIRST_PAGE_STRING = "0"; + + private static final String PAGE_SIZE_STRING = "1"; + + private static final String SEARCH_TERM = "tIo"; + private static final int SECOND_PAGE = 1; + private static final String SECOND_PAGE_STRING = "1"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + /* Response status tests */ + @Test + public void findBySearchTerm_AsAnonymous_ShouldReturnHttpResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isOk()); + } + + + /* No results found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnEmptyPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.numberOfElements", is(0))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnPageThatHasZeroTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + /* One todo entry found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + /* Pagination tests */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnFirstPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(FIRST_PAGE))) + .andExpect(jsonPath("$.first", is(true))) + .andExpect(jsonPath("$.last", is(false))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnSecondTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.Second.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.Second.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.Second.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.Second.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.Second.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.Second.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.Second.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnLastPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(SECOND_PAGE))) + .andExpect(jsonPath("$.first", is(false))) + .andExpect(jsonPath("$.last", is(true))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnFirstTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java index aa8e608..fc0308b 100644 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java @@ -5,6 +5,9 @@ import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; import net.petrikainulainen.springdata.jpa.config.Profiles; import net.petrikainulainen.springdata.jpa.todo.TestUtil; @@ -14,12 +17,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -31,7 +35,8 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; 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.jsonPath; @@ -45,9 +50,9 @@ @ContextConfiguration(classes = {ExampleApplicationContext.class}) @DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, - DbUnitTestExecutionListener.class}) + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) @WebAppConfiguration public class ITUpdateTest { @@ -59,33 +64,53 @@ public class ITUpdateTest { @Before public void setUp() throws SQLException { mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) .build(); } @Test @DatabaseSetup("no-todo-entries.xml") - public void update_TodoEntryNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + public void update_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(status().isNotFound()); } @Test @DatabaseSetup("no-todo-entries.xml") - public void update_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) @@ -95,45 +120,51 @@ public void update_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exc @Test @DatabaseSetup("no-todo-entries.xml") @ExpectedDatabase("no-todo-entries.xml") - public void update_TodoEntryNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_TitleAndDescriptionAreMissing_ShouldReturnResponseStatusBadRequest() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(null) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(null) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(status().isBadRequest()); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_TitleAndDescriptionAreMissing_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(null) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(null) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) @@ -143,55 +174,61 @@ public void update_TitleAndDescriptionAreMissing_ShouldReturnValidationErrorAbou } @Test - @DatabaseSetup("todo-entries.xml") - @ExpectedDatabase("todo-entries.xml") - public void update_TitleAndDescriptionAreMissing_ShouldNotUpdateTodoEntry() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(null) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(null) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_TooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(tooLongDescription) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(tooLongTitle) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(status().isBadRequest()); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(tooLongDescription) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(tooLongTitle) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) @@ -207,74 +244,84 @@ public void update_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutT } @Test - @DatabaseSetup("todo-entries.xml") - @ExpectedDatabase("todo-entries.xml") - public void update_TooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(tooLongDescription) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(tooLongTitle) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_ValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(TodoConstants.UPDATED_DESCRIPTION) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(TodoConstants.UPDATED_TITLE) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(status().isOk()); } @Test - @DatabaseSetup("todo-entries.xml") - public void update_ValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(TodoConstants.UPDATED_DESCRIPTION) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(TodoConstants.UPDATED_TITLE) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ) .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(TodoConstants.CREATION_TIME))) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) .andExpect(jsonPath("$.description", is(TodoConstants.UPDATED_DESCRIPTION))) - .andExpect(jsonPath("$.id", is(TodoConstants.ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", isA(String.class))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) .andExpect(jsonPath("$.title", is(TodoConstants.UPDATED_TITLE))); } @Test - @DatabaseSetup("todo-entries.xml") + @DatabaseSetup("one-todo-entry.xml") @ExpectedDatabase(value = "update-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) - public void update_ValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .description(TodoConstants.UPDATED_DESCRIPTION) - .id(TodoConstants.ID) + .id(TodoConstants.TodoEntries.First.ID) .title(TodoConstants.UPDATED_TITLE) .build(); - mockMvc.perform(put("/api/todo/{id}", TodoConstants.ID) + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) .contentType(WebTestConstants.APPLICATION_JSON_UTF8) .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) ); } } diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/TodoConstants.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/TodoConstants.java deleted file mode 100644 index 701ebb9..0000000 --- a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/TodoConstants.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.springdata.jpa.web; - -/** - * This class contains the constants that are used in our integration tests, DbUnit datasets, - * and the localization file. - * - * @author Petri Kainulainen - */ -final class TodoConstants { - - static final String CREATION_TIME = "2014-12-24T13:13:28+02:00"; - static final String DESCRIPTION = "description"; - static final Long ID = 1L; - static final String MODIFICATION_TIME = "2014-12-25T13:13:28+02:00"; - static final String TITLE = "title"; - - static final String UPDATED_DESCRIPTION = "updatedDescription"; - static final String UPDATED_TITLE = "updatedTitle"; - - static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; - static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; - static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; - static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; - - /** - * Prevents instantiation - */ - private TodoConstants() {} -} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java new file mode 100644 index 0000000..e410ded --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java @@ -0,0 +1,79 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import net.petrikainulainen.springdata.jpa.web.WebTestConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITGetAuthenticatedUserTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void getAuthenticatedUser_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnUserInformationAsJSON() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.username", is("user"))) + .andExpect(jsonPath("$.role", is(UserRole.ROLE_USER.name()))); + } +} diff --git a/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java new file mode 100644 index 0000000..a7d93ff --- /dev/null +++ b/query-methods/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITLoginTest { + + private static final String INVALID_PASSWORD = "invalidPassword"; + private static final String INVALID_USERNAME = "invalidUsername"; + + private static final String PARAM_NAME_PASSWORD = "password"; + private static final String PARAM_NAME_USERNAME = "username"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void logIn_WhenUsernameIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, INVALID_USERNAME) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenPasswordIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, INVALID_PASSWORD) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldReturnResponseStatusFound() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isFound()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldRedirectClientToControllerMethodThatReturnsAuthenticatedUser() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(redirectedUrl("/api/authenticated-user")); + } +} diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml new file mode 100644 index 0000000..45bf713 --- /dev/null +++ b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml index b689168..12e0c00 100644 --- a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml +++ b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml @@ -1,6 +1,10 @@ \ No newline at end of file diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml new file mode 100644 index 0000000..50193f2 --- /dev/null +++ b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml new file mode 100644 index 0000000..0c1e6bc --- /dev/null +++ b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml index edf6f8e..fbb3e27 100644 --- a/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml +++ b/query-methods/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml @@ -1,7 +1,10 @@ \ No newline at end of file diff --git a/query-methods/src/main/ant/build.xml b/query-methods/src/main/ant/build.xml new file mode 100644 index 0000000..90d4c18 --- /dev/null +++ b/query-methods/src/main/ant/build.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java new file mode 100644 index 0000000..6a9566b --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * This class obtains the current time by using a {@link DateTimeService} + * object. The reason for this is that we can use a different implementation in our integration tests. + * + * In other words: + *
    + *
  • + * Our application always returns the correct time because it uses the + * {@link CurrentTimeDateTimeService} class. + *
  • + *
  • + * When our integration tests are running, we can return a constant time which gives us the possibility + * to assert the creation and modification times saved to the database. + *
  • + *
+ * + * @author Petri Kainulainen + */ +public class AuditingDateTimeProvider implements DateTimeProvider { + + private final DateTimeService dateTimeService; + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Calendar getNow() { + return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java new file mode 100644 index 0000000..424e1d4 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * This class is used in our integration tests and it always returns the + * same time. This gives us the possibility to verify that the correct + * timestamps are saved to the database. + * + * @author Petri Kainulainen + */ +public class ConstantDateTimeService implements DateTimeService { + + public static final String CURRENT_DATE_AND_TIME = getConstantDateAndTime(); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConstantDateTimeService.class); + + private static String getConstantDateAndTime() { + return "2015-07-19T12:52:28" + + getSystemZoneOffset() + + getSystemZoneId(); + } + + private static String getSystemZoneOffset() { + return ZonedDateTime.now().getOffset().toString(); + } + + private static String getSystemZoneId() { + return "[" + ZoneId.systemDefault().toString() + "]"; + } + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime constantDateAndTime = ZonedDateTime.from(FORMATTER.parse(CURRENT_DATE_AND_TIME)); + + LOGGER.info("Returning constant date and time: {}", constantDateAndTime); + + return constantDateAndTime; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java new file mode 100644 index 0000000..2812fb0 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +/** + * This class returns the current time. + * + * @author Petri Kainulainen + */ +public class CurrentTimeDateTimeService implements DateTimeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CurrentTimeDateTimeService.class); + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime currentDateAndTime = ZonedDateTime.now(); + + LOGGER.info("Returning current date and time: {}", currentDateAndTime); + + return currentDateAndTime; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java new file mode 100644 index 0000000..a1e1a11 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java @@ -0,0 +1,18 @@ +package net.petrikainulainen.springdata.jpa.common; + +import java.time.ZonedDateTime; + +/** + * This interface defines the methods used to get the current + * date and time. + * + * @author Petri Kainulainen + */ +public interface DateTimeService { + + /** + * Returns the current date and time. + * @return + */ + ZonedDateTime getCurrentDateAndTime(); +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java new file mode 100644 index 0000000..46f2849 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * This controller is responsible of starting the frontend application. + * @author Petri Kainulainen + */ +@Controller +public class FrontendLoaderController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FrontendLoaderController.class); + + private static final String FRONTEND_APPLICATION_VIEW = "frontend/client"; + + /** + * Starts the AngularJS application. + * @return + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public String startAngularJSApplication() { + LOGGER.debug("Starting frontend single page application."); + return FRONTEND_APPLICATION_VIEW; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java new file mode 100644 index 0000000..ed511d8 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java @@ -0,0 +1,34 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/** + * This component returns the username of the authenticated user. + * + * @author Petri Kainulainen + */ +public class UsernameAuditorAware implements AuditorAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(UsernameAuditorAware.class); + + @Override + public String getCurrentAuditor() { + LOGGER.debug("Getting the username of authenticated user."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + LOGGER.debug("Current user is anonymous. Returning null."); + return null; + } + + String username = ((User) authentication.getPrincipal()).getUsername(); + LOGGER.debug("Returning username: {}", username); + + return username; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java index d304ab0..0f922d8 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java @@ -1,5 +1,8 @@ package net.petrikainulainen.springdata.jpa.config; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.common.CurrentTimeDateTimeService; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -15,7 +18,7 @@ */ @Configuration @ComponentScan("net.petrikainulainen.springdata.jpa") -@Import({WebMvcContext.class, PersistenceContext.class}) +@Import({WebMvcContext.class, PersistenceContext.class, SecurityContext.class}) public class ExampleApplicationContext { private static final String MESSAGE_SOURCE_BASE_NAME = "i18n/messages"; @@ -31,11 +34,23 @@ public class ExampleApplicationContext { @PropertySource("classpath:application.properties") static class ApplicationProperties {} + @Profile(Profiles.APPLICATION) + @Bean + DateTimeService currentTimeDateTimeService() { + return new CurrentTimeDateTimeService(); + } + @Profile(Profiles.INTEGRATION_TEST) @Configuration @PropertySource("classpath:integration-test.properties") static class IntegrationTestProperties {} + @Profile(Profiles.INTEGRATION_TEST) + @Bean + DateTimeService constantDateTimeService() { + return new ConstantDateTimeService(); + } + @Bean MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java index 6daec41..78a9a36 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java @@ -2,10 +2,17 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import net.petrikainulainen.springdata.jpa.common.AuditingDateTimeProvider; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import net.petrikainulainen.springdata.jpa.common.UsernameAuditorAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -27,10 +34,12 @@ * @author Petri Kainulainen */ @Configuration +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") @EnableJpaRepositories(basePackages = { "net.petrikainulainen.springdata.jpa.todo" }) @EnableTransactionManagement +@EnableSpringDataWebSupport class PersistenceContext { private static final String[] ENTITY_PACKAGES = { "net.petrikainulainen.springdata.jpa.todo" @@ -46,6 +55,16 @@ class PersistenceContext { private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; + @Bean + AuditorAware auditorProvider() { + return new UsernameAuditorAware(); + } + + @Bean + DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { + return new AuditingDateTimeProvider(dateTimeService); + } + /** * Creates and configures the HikariCP datasource bean. * @param env The runtime environment of our application. diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java new file mode 100644 index 0000000..8aa95e4 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.web.security.CsrfHeaderFilter; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationEntryPoint; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationFailureHandler; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationSuccessHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CsrfFilter; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebSecurity +class SecurityContext extends WebSecurityConfigurerAdapter { + + @Bean + AuthenticationEntryPoint authenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + AuthenticationFailureHandler authenticationFailureHandler() { + return new RestAuthenticationFailureHandler(); + } + + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler() { + return new RestAuthenticationSuccessHandler(); + } + + @Bean + protected UserDetailsService userDetailsService() { + return super.userDetailsService(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + //Use the custom authentication entry point. + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint()) + .and() + //Configure form login. + .formLogin() + .loginProcessingUrl("/api/login") + .failureHandler(authenticationFailureHandler()) + .successHandler(authenticationSuccessHandler()) + .permitAll() + .and() + //Configure logout function. + .logout() + .deleteCookies("JSESSIONID") + .logoutUrl("/api/logout") + .logoutSuccessUrl("/") + .and() + //Configure url based authorization + .authorizeRequests() + .antMatchers( + "/", + "/api/csrf" + ).permitAll() + .anyRequest().hasRole("USER") + .and() + .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + //Spring Security ignores request to static resources such as CSS or JS files. + .ignoring() + .antMatchers( + "/favicon.ico", + "/css/**", + "/i18n/**", + "/js/**" + ); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java index fb918c5..f1861a6 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java @@ -5,6 +5,7 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.DispatcherType; @@ -35,7 +36,9 @@ public void onStartup(ServletContext servletContext) throws ServletException { configureDispatcherServlet(servletContext, rootContext); EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + configureCharacterEncodingFilter(servletContext, dispatcherTypes); + configureSpringSecurityFilter(servletContext, dispatcherTypes); servletContext.addListener(new ContextLoaderListener(rootContext)); } @@ -55,4 +58,9 @@ private void configureCharacterEncodingFilter(ServletContext servletContext, Enu FilterRegistration.Dynamic characterEncoding = servletContext.addFilter(CHARACTER_ENCODING_FILTER_NAME, characterEncodingFilter); characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, CHARACTER_ENCODING_FILTER_URL_PATTERN); } + + private void configureSpringSecurityFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); + security.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); + } } diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java index 85f60df..c016860 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java @@ -9,6 +9,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import java.util.List; @@ -39,4 +40,9 @@ public void configureMessageConverters(List> converters) converters.add(converter); } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp("/WEB-INF/jsp/", ".jsp"); + } } diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java new file mode 100644 index 0000000..80abd33 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java @@ -0,0 +1,41 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoSearchService implements TodoSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoSearchService.class); + + private final TodoRepository repository; + + @Autowired + public RepositoryTodoSearchService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + @Override + public Page findBySearchTerm(String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = repository.findBySearchTerm(searchTerm, pageRequest); + + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return TodoMapper.mapEntityPageIntoDTOPage(pageRequest, searchResultPage); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java index 0423e83..f59be0d 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java @@ -9,8 +9,6 @@ import java.util.List; import java.util.Optional; -import static java.util.stream.Collectors.toList; - /** * @author Petri Kainulainen */ @@ -39,7 +37,7 @@ public TodoDTO create(TodoDTO newTodoEntry) { created = repository.save(created); LOGGER.info("Created a new todo entry: {}", created); - return transformIntoDTO(created); + return TodoMapper.mapEntityIntoDTO(created); } @Transactional @@ -53,7 +51,7 @@ public TodoDTO delete(Long id) { repository.delete(deleted); LOGGER.info("Deleted todo entry: {}", deleted); - return transformIntoDTO(deleted); + return TodoMapper.mapEntityIntoDTO(deleted); } @Transactional(readOnly = true) @@ -65,13 +63,7 @@ public List findAll() { LOGGER.info("Found {} todo entries", todoEntries.size()); - return transformIntoDTOs(todoEntries); - } - - private List transformIntoDTOs(List entities) { - return entities.stream() - .map(this::transformIntoDTO) - .collect(toList()); + return TodoMapper.mapEntitiesIntoDTOs(todoEntries); } @Transactional(readOnly = true) @@ -82,7 +74,7 @@ public TodoDTO findById(Long id) { Todo todoEntry = findTodoEntryById(id); LOGGER.info("Found todo entry: {}", todoEntry); - return transformIntoDTO(todoEntry); + return TodoMapper.mapEntityIntoDTO(todoEntry); } @Transactional @@ -92,26 +84,18 @@ public TodoDTO update(TodoDTO updatedTodoEntry) { Todo updated = findTodoEntryById(updatedTodoEntry.getId()); updated.update(updatedTodoEntry.getTitle(), updatedTodoEntry.getDescription()); + + //We need to flush the changes or otherwise the returned object + //doesn't contain the updated audit information. + repository.flush(); + LOGGER.info("Updated the information of the todo entry: {}", updated); - return transformIntoDTO(updated); + return TodoMapper.mapEntityIntoDTO(updated); } private Todo findTodoEntryById(Long id) { Optional todoResult = repository.findOne(id); return todoResult.orElseThrow(() -> new TodoNotFoundException(id)); } - - private TodoDTO transformIntoDTO(Todo entity) { - TodoDTO dto = new TodoDTO(); - - dto.setCreationTime(entity.getCreationTime()); - dto.setDescription(entity.getDescription()); - dto.setId(entity.getId()); - dto.setModificationTime(entity.getModificationTime()); - dto.setTitle(entity.getTitle()); - - return dto; - } - } diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java index f34f6bd..b3acfbb 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java @@ -2,13 +2,20 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.hibernate.annotations.Type; +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 javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.PrePersist; +import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedQuery; import javax.persistence.Table; import javax.persistence.Version; import java.time.ZonedDateTime; @@ -25,6 +32,20 @@ * @author Petri Kainulainen */ @Entity +@EntityListeners(AuditingEntityListener.class) +@NamedNativeQuery(name = "Todo.findBySearchTermNamedNative", + query="SELECT * FROM todos t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC", + resultClass = Todo.class +) +@NamedQuery(name = "Todo.findBySearchTermNamed", + query = "SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) " + + "ORDER BY t.title ASC" +) @Table(name = "todos") final class Todo { @@ -35,15 +56,25 @@ final class Todo { @GeneratedValue(strategy = GenerationType.AUTO) private Long id; + @Column(name = "created_by_user", nullable = false) + @CreatedBy + private String createdByUser; + @Column(name = "creation_time", nullable = false) @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @CreatedDate private ZonedDateTime creationTime; @Column(name = "description", length = MAX_LENGTH_DESCRIPTION) private String description; + @Column(name = "modified_by_user", nullable = false) + @LastModifiedBy + private String modifiedByUser; + @Column(name = "modification_time") @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @LastModifiedDate private ZonedDateTime modificationTime; @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE) @@ -70,6 +101,10 @@ Long getId() { return id; } + String getCreatedByUser() { + return createdByUser; + } + ZonedDateTime getCreationTime() { return creationTime; } @@ -78,6 +113,10 @@ String getDescription() { return description; } + String getModifiedByUser() { + return modifiedByUser; + } + ZonedDateTime getModificationTime() { return modificationTime; } @@ -90,13 +129,6 @@ long getVersion() { return version; } - @PrePersist - public void prePersist() { - ZonedDateTime now = ZonedDateTime.now(); - this.creationTime = now; - this.modificationTime = now; - } - void update(String newTitle, String newDescription) { requireValidTitleAndDescription(newTitle, newDescription); @@ -121,9 +153,11 @@ private void requireValidTitleAndDescription(String title, String description) { @Override public String toString() { return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) .append("creationTime", this.creationTime) .append("description", this.description) .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) .append("modificationTime", this.modificationTime) .append("title", this.title) .append("version", this.version) diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java index 7f24552..7eea8d2 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java @@ -11,6 +11,8 @@ */ public final class TodoDTO { + private String createdByUser; + private ZonedDateTime creationTime; @Size(max = Todo.MAX_LENGTH_DESCRIPTION) @@ -18,6 +20,8 @@ public final class TodoDTO { private Long id; + private String modifiedByUser; + private ZonedDateTime modificationTime; @NotEmpty @@ -26,6 +30,10 @@ public final class TodoDTO { public TodoDTO() {} + public String getCreatedByUser() { + return createdByUser; + } + public ZonedDateTime getCreationTime() { return creationTime; } @@ -38,6 +46,10 @@ public Long getId() { return id; } + public String getModifiedByUser() { + return modifiedByUser; + } + public ZonedDateTime getModificationTime() { return modificationTime; } @@ -46,6 +58,10 @@ public String getTitle() { return title; } + public void setCreatedByUser(String createdByUser) { + this.createdByUser = createdByUser; + } + public void setCreationTime(ZonedDateTime creationTime) { this.creationTime = creationTime; } @@ -58,6 +74,10 @@ public void setId(Long id) { this.id = id; } + public void setModifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + public void setModificationTime(ZonedDateTime modificationTime) { this.modificationTime = modificationTime; } @@ -69,9 +89,11 @@ public void setTitle(String title) { @Override public String toString() { return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) .append("creationTime", this.creationTime) .append("description", this.description) .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) .append("modificationTime", this.modificationTime) .append("title", this.title) .toString(); diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java new file mode 100644 index 0000000..0ccb5e2 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is a mapper class that is used to transform {@link Todo} objects + * into {@link TodoDTO} objects. + * @author Petri Kainulainen + */ +final class TodoMapper { + + /** + * Transforms the list of {@link Todo} objects given as a method parameter + * into a list of {@link TodoDTO} objects and returns the created list. + * + * @param entities + * @return + */ + static List mapEntitiesIntoDTOs(Iterable entities) { + List dtos = new ArrayList<>(); + + entities.forEach(e -> dtos.add(mapEntityIntoDTO(e))); + + return dtos; + } + + /** + * Transforms the {@link Todo} object given as a method parameter into a + * {@link TodoDTO} object and returns the created object. + * + * @param entity + * @return + */ + static TodoDTO mapEntityIntoDTO(Todo entity) { + TodoDTO dto = new TodoDTO(); + + dto.setCreatedByUser(entity.getCreatedByUser()); + dto.setCreationTime(entity.getCreationTime()); + dto.setDescription(entity.getDescription()); + dto.setId(entity.getId()); + dto.setModifiedByUser(entity.getModifiedByUser()); + dto.setModificationTime(entity.getModificationTime()); + dto.setTitle(entity.getTitle()); + + return dto; + } + + /** + * Transforms {@code Page} objects into {@code Page} objects. + * @param pageRequest The information of the requested page. + * @param source The {@code Page} object. + * @return The created {@code Page} object. + */ + static Page mapEntityPageIntoDTOPage(Pageable pageRequest, Page source) { + List dtos = mapEntitiesIntoDTOs(source.getContent()); + return new PageImpl<>(dtos, pageRequest, source.getTotalElements()); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java index 6cc2c77..359647e 100644 --- a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java @@ -1,6 +1,11 @@ package net.petrikainulainen.springdata.jpa.todo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -17,7 +22,128 @@ interface TodoRepository extends Repository { List findAll(); + /** + * This query method creates the invoked query method by parsing it from the method name of the query method. + * @param descriptionPart The part that must be found from the description of the todo entry. + * @param titlePart The part that must be found from the title of the todo entry. + * @param pageRequest The information of the requested page. + * @return A page of todo entries whose title or description contains with the given search term. The content of + * the returned page depends from the page request given as a method parameter. + */ + Page findByDescriptionContainsOrTitleContainsAllIgnoreCase(String descriptionPart, + String titlePart, + Pageable pageRequest); + + /** + * This query method creates the invoked query method by parsing it from the method name of the query method. + * @param descriptionPart The part that must be found from the description of the todo entry. + * @param titlePart The part that must be found from the title of the todo entry. + * @return A list of todo entries whose title or description contains with the given search criteria. The returned + * todo entries are sorted in alphabetical order by using the title of the todo entry. + */ + List findByDescriptionContainsOrTitleContainsAllIgnoreCaseOrderByTitleAsc(String descriptionPart, + String titlePart); + + /** + * This query method invokes the named JPQL query that is configured in the {@code Todo} class by using the + * {@code @NamedQuery} annotation. The name of the named query is: {@code Todo.findBySearchTermNamed}. + * @param searchTerm The given search term. + * @param pageRequest The information of the given page. + * @return A page of todo entries whose title or description contains with the given search term. The content of + * the returned page depends from the page request given as a method parameter. + */ + Page findBySearchTermNamed(@Param("searchTerm") String searchTerm, Pageable pageRequest); + + /** + * This query method invokes the named SQL query that is configured in the {@code Todo} class by using + * the {@code @NamedNativeQuery} annotation. The name of the named native query is: {@code Todo.findBySearchTermNamedNative}. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. The returned + * todo entries are sorted in alphabetical order by using the title of the todo entry. + */ + List findBySearchTermNamedNative(@Param("searchTerm") String searchTerm); + + /** + * This query method reads the named JPQL query from the {@code META-INF/jpa-named-queries.properties} file. + * The name of the invoked query is: {@code Todo.findBySearchTermNamedFile}. + * @param searchTerm The given search term. + * @param pageRequest The information of the given page. + * @return A page of todo entries whose title or description contains with the given search term. The content of + * the returned page depends from the page request given as a method parameter. + */ + Page findBySearchTermNamedFile(@Param("searchTerm") String searchTerm, Pageable pageRequest); + + /** + * This query method reads the named native query from the {@code META-INF/jpa-named-queries.properties} file. + * The name of the invoked query is: {@code Todo.findBySearchTermNamedNativeFile}. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. The returned + * todo entries are sorted in alphabetical order by using the title of the todo entry. + */ + @Query(nativeQuery = true) + List findBySearchTermNamedNativeFile(@Param("searchTerm") String searchTerm); + + /** + * This query method reads the named from the {@code META-INF/orm.xml} file. The name of the invoked query + * is: {@code Todo.findBySearchTermNamedOrmXml}. + * @param searchTerm The given search term. + * @param pageRequest The information of the given page. + * @return A page of todo entries whose title or description contains with the given search term. The content of + * the returned page depends from the page request given as a method parameter. + */ + Page findBySearchTermNamedOrmXml(@Param("searchTerm") String searchTerm, Pageable pageRequest); + + /** + * This query method reads the named from the {@code META-INF/orm.xml} file. The name of the invoked query + * is: {@code Todo.findBySearchTermNamedNativeOrmXml}. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains the given search term. The returned + * todo entries are sorted in alphabetical order by using the title of the todo entry. + */ + @Query(nativeQuery = true) + List findBySearchTermNamedNativeOrmXml(@Param("searchTerm") String searchTerm); + + /** + * This query method invokes the JPQL query that is configured by using the {@code @Query} annotation. + * @param searchTerm The given search term. + * @param pageRequest The information of the requested page. + * @return A page of todo entries whose title or description contains with the given search term. The content of + * the returned page depends from the page request given as a method parameter. + */ + @Query("SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))") + Page findBySearchTerm(@Param("searchTerm") String searchTerm, Pageable pageRequest); + + /** + * This query method invokes the JPQL query that is configured by using the {@code @Query} annotation. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. The + * returned todo entries are sorted in alphabetical order by using the title of a todo entry. + */ + @Query("SELECT t FROM Todo t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC") + List findBySearchTermSortedInQuery(@Param("searchTerm") String searchTerm); + + /** + * This query method invokes the SQL query that is configured by using the {@code @Query} annotation. + * @param searchTerm The given search term. + * @return A list of todo entries whose title or description contains with the given search term. The + * returned todo entries are sorted in alphabetical order by using the title of a todo entry. + */ + @Query(value = "SELECT * FROM todos t WHERE " + + "LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + + "LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) " + + "ORDER BY t.title ASC", + nativeQuery = true + ) + List findBySearchTermNative(@Param("searchTerm") String searchTerm); + Optional findOne(Long id); + void flush(); + Todo save(Todo persisted); } diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java new file mode 100644 index 0000000..8d17d6d --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java @@ -0,0 +1,22 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * This service provides finder methods for {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +public interface TodoSearchService { + + /** + * Finds todo entries whose title or description contains the given search term. + * This search is case insensitive. + * @param searchTerm The search term. + * @param pageRequest The information of the requested page. + * @return A list of todo entries whose title or description contains the given search term. The returned + * list is sorted by using the sort specification given as a method parameter. + */ + Page findBySearchTerm(String searchTerm, Pageable pageRequest); +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java new file mode 100644 index 0000000..c365828 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to find todo entries by using + * different search criteria. + * + * @author Petri Kainulainen + */ +@RestController +final class TodoSearchController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoSearchController.class); + + private final TodoSearchService searchService; + + @Autowired + public TodoSearchController(TodoSearchService searchService) { + this.searchService = searchService; + } + + /** + * Finds todo entries whose title or description contains the given search term. This + * search is case insensitive. + * @param searchTerm The used search term. + * @param pageRequest The information of the requested page. + * @return + */ + @RequestMapping(value = "/api/todo/search", method = RequestMethod.GET) + public Page findBySearchTerm(@RequestParam("searchTerm") String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = searchService.findBySearchTerm(searchTerm, pageRequest); + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return searchResultPage; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java new file mode 100644 index 0000000..b02059e --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of an error that occurred when the API tried + * to perform the operation requested by the client. + * + * @author Petri Kainulainen + */ +final class ErrorDTO { + + private final String code; + private final String message; + + ErrorDTO(String code, String message) { + notNull(code, "Code cannot be null."); + notEmpty(code, "Code cannot be empty."); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty"); + + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java new file mode 100644 index 0000000..44234a5 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of a single field error. + * + * @author Petri Kainulainen + */ +final class FieldErrorDTO { + + private final String field; + + private final String message; + + FieldErrorDTO(String field, String message) { + notNull(field, "Field cannot be null."); + notEmpty(field, "Field cannot be empty"); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty."); + + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java new file mode 100644 index 0000000..5ad9e9b --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; +import java.util.Locale; + +/** + * This class handles the exceptions thrown by our REST API. + * + * @author Petri Kainulainen + */ +@ControllerAdvice +public final class RestErrorHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class); + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + + private final MessageSource messageSource; + + @Autowired + public RestErrorHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * Processes an error that occurs when the requested todo entry is not found. + * @param ex The exception that was thrown when the todo entry was not found. + * @param currentLocale The current locale. + * @return An error object that contains the error code and message. + */ + @ExceptionHandler(TodoNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + ErrorDTO handleTodoEntryNotFound(TodoNotFoundException ex, Locale currentLocale) { + LOGGER.error("Todo entry was not found by using id: {}", ex.getId()); + + MessageSourceResolvable errorMessageRequest = createSingleErrorMessageRequest( + ERROR_CODE_TODO_ENTRY_NOT_FOUND, + ex.getId() + ); + + String errorMessage = messageSource.getMessage(errorMessageRequest, currentLocale); + return new ErrorDTO(HttpStatus.NOT_FOUND.name(), errorMessage); + } + + private DefaultMessageSourceResolvable createSingleErrorMessageRequest(String errorMessageCode, Object... params) { + return new DefaultMessageSourceResolvable(new String[] {errorMessageCode}, params); + } + + /** + * Processes an error that occurs when the validation of an object fails. + * + * @param ex The exception that was thrown when the validation failed. + * @return An error object that describes all validation errors. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ValidationErrorDTO handleValidationErrors(MethodArgumentNotValidException ex, Locale currentLocale) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result.getFieldErrors(); + LOGGER.error("Found {} validation errors", fieldErrors.size()); + + return constructValidationErrors(fieldErrors, currentLocale); + } + + private ValidationErrorDTO constructValidationErrors(List fieldErrors, Locale currentLocale) { + ValidationErrorDTO dto = new ValidationErrorDTO(); + + for (FieldError fieldError: fieldErrors) { + String localizedErrorMessage = getValidationErrorMessage(fieldError, currentLocale); + dto.addFieldError(fieldError.getField(), localizedErrorMessage); + } + + return dto; + } + + private String getValidationErrorMessage(FieldError fieldError, Locale currentLocale) { + String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale); + + //If the message was not found, return the most accurate field error code instead. + //You can remove this check if you prefer to get the default error message. + if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) { + String[] fieldErrorCodes = fieldError.getCodes(); + localizedErrorMessage = fieldErrorCodes[0]; + } + + return localizedErrorMessage; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java new file mode 100644 index 0000000..8355c7b --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import org.springframework.http.HttpStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class contains the information of validation errors that are found + * from a controller method parameter that is annotated with the + * {@link javax.validation.Valid} annotation. + * + * @author Petri Kainulainen + */ +final class ValidationErrorDTO { + + private final String code = HttpStatus.BAD_REQUEST.name(); + + private final List fieldErrors = new ArrayList<>(); + + ValidationErrorDTO() { + } + + void addFieldError(String field, String message) { + FieldErrorDTO error = new FieldErrorDTO(field, message); + fieldErrors.add(error); + } + + public String getCode() { + return code; + } + + public List getFieldErrors() { + return fieldErrors; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java new file mode 100644 index 0000000..141a948 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java @@ -0,0 +1,46 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This filter reads the {@link org.springframework.security.web.csrf.CsrfToken} from the {@link HttpServletRequest} and + * sets its content to the {@link HttpServletResponse} headers. + * + * I borrowed this idea from this StackOverflow question. + * + * @author Petri Kainulainen + */ +public class CsrfHeaderFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(CsrfHeaderFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + LOGGER.trace("Reading CSRF token from the request."); + + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (token != null) { + LOGGER.trace("CSRF token was found. Creating HTTP response headers."); + response.setHeader("X-CSRF-HEADER", token.getHeaderName()); + response.setHeader("X-CSRF-PARAM", token.getParameterName()); + response.setHeader("X-CSRF-TOKEN", token.getToken()); + } + else { + LOGGER.trace("CSRF Token was not found. Doing nothing."); + } + + filterChain.doFilter(request, response); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java new file mode 100644 index 0000000..f6e70cb --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Petri Kainulainen + */ +@RestController +public class CsrfTokenController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsrfTokenController.class); + + @RequestMapping(value = "/api/csrf", method = RequestMethod.HEAD) + public void getCsrfToken() { + LOGGER.info("Getting CSRF token."); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..887e25b --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication entry point returns the HTTP status code 401. + * @author Petri Kainulainen + */ +public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + LOGGER.info("Authentication required. Returning HTTP status code 401."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..daf635b --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication failure handler returns the HTTP status code 403. + * @author Petri Kainulainen + */ +public final class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationFailureHandler.class); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + LOGGER.info("Authentication failed with message: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failed."); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ff84785 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication success handler returns the information of the authenticated + * user as JSON. + * + * @author Petri Kainulainen + */ +public final class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + LOGGER.info("Authentication was successful"); + response.sendRedirect(response.encodeRedirectURL("/api/authenticated-user")); + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java new file mode 100644 index 0000000..ef7959d --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java @@ -0,0 +1,45 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to return the information + * of the authenticated user. + * + * @author Petri Kainulainen + */ +@RestController +final class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + /** + * Returns the information of the authenticated user as JSON. The returned information + * contains the username and the user role of the authenticated user. + * + * @param authenticatedUser The information of the authenticated user. + * @return + */ + @RequestMapping(value = "/api/authenticated-user", method = RequestMethod.GET) + public UserDTO getAuthenticatedUser(@AuthenticationPrincipal User authenticatedUser) { + LOGGER.info("Getting authenticated user."); + + if (authenticatedUser == null) { + //If anonymous users can access this controller method, someone has changed + //the security configuration and it must be fixed. + LOGGER.error("Authenticated user is not found."); + throw new AccessDeniedException("Anonymous users cannot request the information of the authenticated user."); + } + else { + LOGGER.info("User with username: {} is authenticated", authenticatedUser.getUsername()); + return new UserDTO(authenticatedUser.getUsername(), authenticatedUser.getAuthorities()); + } + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java new file mode 100644 index 0000000..92b99ed --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import net.petrikainulainen.springdata.jpa.common.PreCondition; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * This class contains the information of the authenticated user. + * + * @author Petri Kainulainen + */ +public final class UserDTO { + + private final String username; + + private final UserRole role; + + UserDTO(String username, Collection authorities) { + PreCondition.isTrue(!username.isEmpty(), "Username cannot be empty."); + PreCondition.isTrue(authorities.size() == 1, "User must have only one granted authority."); + this.username = username; + + GrantedAuthority authority = authorities.iterator().next(); + this.role = UserRole.valueOf(authority.getAuthority()); + } + + public String getUsername() { + return username; + } + + public UserRole getRole() { + return role; + } +} diff --git a/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java new file mode 100644 index 0000000..8b3e6a6 --- /dev/null +++ b/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java @@ -0,0 +1,8 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +/** + * @author Petri Kainulainen + */ +enum UserRole { + ROLE_USER +} diff --git a/query-methods/src/main/resources/META-INF/jpa-named-queries.properties b/query-methods/src/main/resources/META-INF/jpa-named-queries.properties new file mode 100644 index 0000000..97d737e --- /dev/null +++ b/query-methods/src/main/resources/META-INF/jpa-named-queries.properties @@ -0,0 +1,2 @@ +Todo.findBySearchTermNamedFile=SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC +Todo.findBySearchTermNamedNativeFile=SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC \ No newline at end of file diff --git a/query-methods/src/main/resources/META-INF/orm.xml b/query-methods/src/main/resources/META-INF/orm.xml new file mode 100644 index 0000000..cc2bf80 --- /dev/null +++ b/query-methods/src/main/resources/META-INF/orm.xml @@ -0,0 +1,16 @@ + + + + + SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC + + + + SELECT * FROM todos t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%')) ORDER BY t.title ASC + + \ No newline at end of file diff --git a/query-methods/src/main/resources/applicationContext-persistence.xml b/query-methods/src/main/resources/applicationContext-persistence.xml index fb18c96..e9149e9 100644 --- a/query-methods/src/main/resources/applicationContext-persistence.xml +++ b/query-methods/src/main/resources/applicationContext-persistence.xml @@ -76,5 +76,12 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/t + + + + + + + \ No newline at end of file diff --git a/query-methods/src/main/resources/applicationContext-web.xml b/query-methods/src/main/resources/applicationContext-web.xml index 3bac53f..db48af6 100644 --- a/query-methods/src/main/resources/applicationContext-web.xml +++ b/query-methods/src/main/resources/applicationContext-web.xml @@ -5,7 +5,34 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans%20http://www.springframework.org/schema/beans/spring-beans.xsd%20http://www.springframework.org/schema/mvc%20http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> - + + + + + + + + + WRITE_DATES_AS_TIMESTAMPS + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/query-methods/src/main/resources/applicationContext.xml b/query-methods/src/main/resources/applicationContext.xml index 8b78c3d..b9ee424 100644 --- a/query-methods/src/main/resources/applicationContext.xml +++ b/query-methods/src/main/resources/applicationContext.xml @@ -8,7 +8,9 @@ + + @@ -18,6 +20,9 @@ + + @@ -25,6 +30,9 @@ + + diff --git a/query-methods/src/main/webapp/WEB-INF/jsp/frontend/client.jsp b/query-methods/src/main/webapp/WEB-INF/jsp/frontend/client.jsp new file mode 100644 index 0000000..84158d0 --- /dev/null +++ b/query-methods/src/main/webapp/WEB-INF/jsp/frontend/client.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ +
+
+

+

+
+
+ + + diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java new file mode 100644 index 0000000..c51749a --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Petri Kainulainen + */ +public class PageBuilder { + + private List elements = new ArrayList<>(); + private Pageable pageRequest; + private int totalElements; + + public PageBuilder() {} + + public PageBuilder elements(List elements) { + this.elements = elements; + return this; + } + + public PageBuilder pageRequest(Pageable pageRequest) { + this.pageRequest = pageRequest; + return this; + } + + public PageBuilder totalElements(int totalElements) { + this.totalElements = totalElements; + return this; + } + + public Page build() { + return new PageImpl(elements, pageRequest, totalElements); + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java index 5beae38..7e90183 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java @@ -2,8 +2,7 @@ import org.junit.Test; -import static net.petrikainulainen.springdata.jpa.common.ThrowableCaptor.thrown; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Petri Kainulainen @@ -19,9 +18,7 @@ public void isTrueWithDynamicErrorMessage_ExpressionIsTrue_ShouldNotThrowExcepti @Test public void isTrueWithDynamicErrorMessage_ExpressionIsFalse_ShouldThrowException() { - Throwable thrown = thrown(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)); - - assertThat(thrown) + assertThatThrownBy(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)) .isExactlyInstanceOf(IllegalArgumentException.class) .hasMessage("Dynamic error message with parameter: 1"); } @@ -33,9 +30,7 @@ public void isTrueWithStaticErrorMessage_ExpressionIsTrue_ShouldNotThrowExceptio @Test public void isTrueWithStaticErrorMessage_ExpressionIsFalse_ShouldThrowException() { - Throwable thrown = thrown(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)); - - assertThat(thrown) + assertThatThrownBy(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)) .isExactlyInstanceOf(IllegalArgumentException.class) .hasMessage(STATIC_ERROR_MESSAGE); } @@ -47,9 +42,7 @@ public void notEmpty_StringIsNotEmpty_ShouldNotThrowException() { @Test public void notEmpty_StringIsEmpty_ShouldThrowException() { - Throwable thrown = thrown(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)); - - assertThat(thrown) + assertThatThrownBy(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)) .isExactlyInstanceOf(IllegalArgumentException.class) .hasMessage(STATIC_ERROR_MESSAGE); } @@ -61,9 +54,7 @@ public void notNull_ObjectIsNotNull_ShouldNotThrowException() { @Test public void notNull_ObjectIsNull_ShouldThrowException() { - Throwable thrown = thrown(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)); - - assertThat(thrown) + assertThatThrownBy(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)) .isExactlyInstanceOf(NullPointerException.class) .hasMessage(STATIC_ERROR_MESSAGE); } diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/ThrowableCaptor.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/ThrowableCaptor.java deleted file mode 100644 index 6e8cebe..0000000 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/common/ThrowableCaptor.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.petrikainulainen.springdata.jpa.common; - -/** - * This class is used to capture the Throwable object thrown by the tested - * method. - * - * Note: I borrowed this idea from a blog post titled: - * Clean JUnit Throwable-Tests with Java 8 Lambdas - * @author Petri Kainulainen - */ -public final class ThrowableCaptor { - - @FunctionalInterface - public interface Actor { - void act() throws Throwable; - } - - /** - * Prevents instantiation. - */ - private ThrowableCaptor() {} - - /** - * Captures the thrown Throwable object. - * @param actor - * @return The captured Throwable object of null if none is thrown. - */ - public static Throwable thrown(Actor actor) { - Throwable thrown = null; - try { - actor.act(); - } - catch (Throwable captured) { - thrown = captured; - } - return thrown; - } -} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java new file mode 100644 index 0000000..3086296 --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java @@ -0,0 +1,162 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.Arrays; + +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoSearchServiceTest { + + private static final String SEARCH_TERM = "itl"; + + private TodoRepository repository; + private RepositoryTodoSearchService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoSearchService(repository); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final int PAGE_SIZE = 5; + private final String SORT_PROPERTY = "title"; + + private Pageable pageRequest; + + @Before + public void createPageRequest() { + Sort sort = new Sort(Sort.Direction.ASC, SORT_PROPERTY); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findBySearchTerm(eq(SEARCH_TERM), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageWithRequestedPageNumber() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumber()).isEqualTo(PAGE_NUMBER); + } + + @Test + public void shouldReturnPageWithRequestedPageSize() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSize()).isEqualTo(PAGE_SIZE); + } + + @Test + public void shouldReturnPageThatIsSortedInAscendingOrderByUsingSortProperty() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSort().getOrderFor(SORT_PROPERTY).getDirection()) + .isEqualTo(Sort.Direction.ASC); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findBySearchTerm(eq(SEARCH_TERM), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnEmptyPage() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void shouldReturnPageWithTotalElementCountZero() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + } + + public class WhenOneTodoEntryIsFound { + + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final Long ID = 20L; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Page resultPage = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + + given(repository.findBySearchTerm(eq(SEARCH_TERM), eq(pageRequest))).willReturn(resultPage); + } + + @Test + public void shouldReturnPageThatHasOneTodoEntry() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + } + + @Test + public void shouldReturnPageThatHasCorrectInformation() { + TodoDTO found = service.findBySearchTerm(SEARCH_TERM, pageRequest).getContent().get(0); + + assertThatTodoDTO(found) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnPageWithTotalElementCountOne() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java index 4228731..81bbdb5 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java @@ -4,17 +4,17 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import static net.petrikainulainen.springdata.jpa.common.ThrowableCaptor.thrown; +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; @@ -29,9 +29,11 @@ @RunWith(NestedRunner.class) public class RepositoryTodoServiceTest { + private static final String CREATED_BY_USER = "createdByUser"; private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; private static final String DESCRIPTION = "description"; private static final Long ID = 20L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; private static final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; private static final String TITLE = "title"; @@ -50,12 +52,23 @@ public void setUp() { public class Create { - @Test - public void shouldPersistNewTodoEntryWithCorrectInformation() { + @Before + public void returnNewTodoEntry() { given(repository.save(isA(Todo.class))).willAnswer( - invocationOnMock -> invocationOnMock.getArguments()[0] + invocationOnMock -> new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build() ); + } + @Test + public void shouldPersistNewTodoEntryWithCorrectInformation() { TodoDTO newTodoEntry = new TodoDTOBuilder() .description(DESCRIPTION) .title(TITLE) @@ -63,31 +76,20 @@ public void shouldPersistNewTodoEntryWithCorrectInformation() { service.create(newTodoEntry); - ArgumentCaptor persistedArgument = ArgumentCaptor.forClass(Todo.class); - verify(repository, times(1)).save(persistedArgument.capture()); + verify(repository, times(1)).save( + assertArg(persisted -> assertThatTodoEntry(persisted) + .hasNoCreationAuditFieldValues() + .hasDescription(DESCRIPTION) + .hasNoId() + .hasNoModificationAuditFieldValues() + .hasTitle(TITLE) + ) + ); verifyNoMoreInteractions(repository); - - Todo persisted = persistedArgument.getValue(); - assertThatTodoEntry(persisted) - .hasNoCreationTime() - .hasDescription(DESCRIPTION) - .hasNoId() - .hasNoModificationTime() - .hasTitle(TITLE); } @Test public void shouldReturnTheInformationOfPersistedTodoEntry() { - given(repository.save(isA(Todo.class))).willAnswer( - invocationOnMock -> new TodoBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build() - ); - TodoDTO newTodoEntry = new TodoDTOBuilder() .description(DESCRIPTION) .title(TITLE) @@ -99,7 +101,9 @@ public void shouldReturnTheInformationOfPersistedTodoEntry() { .hasId(ID) .hasTitle(TITLE) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } } @@ -107,11 +111,15 @@ public class Delete { public class WhenTodoEntryIsNotFound { - @Test - public void shouldThrowExceptionWithCorrectId() { + @Before + public void returnNoTodoEntry() { given(repository.findOne(ID)).willReturn(Optional.empty()); - Throwable thrown = thrown(() -> service.delete(ID)); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.delete(ID)); assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); @@ -121,9 +129,7 @@ public void shouldThrowExceptionWithCorrectId() { @Test public void shouldNotDeleteTodoEntry() { - given(repository.findOne(ID)).willReturn(Optional.empty()); - - thrown(() -> service.delete(ID)); + catchThrowable(() -> service.delete(ID)); verify(repository, never()).delete(isA(Todo.class)); } @@ -131,28 +137,32 @@ public void shouldNotDeleteTodoEntry() { public class WhenTodoEntryIsFound { - @Test - public void shouldDeleteFoundTodoEntry() { - Todo found = new TodoBuilder().build(); - given(repository.findOne(ID)).willReturn(Optional.of(found)); - - service.delete(ID); + private Todo deleted; - verify(repository, times(1)).delete(found); - } - - @Test - public void shouldReturnTheInformationOfDeletedTodoEntry() { - Todo found = new TodoBuilder() + @Before + public void returnDeletedTodoEntry() { + deleted = new TodoBuilder() + .createdByUser(CREATED_BY_USER) .creationTime(CREATION_TIME) .description(DESCRIPTION) .id(ID) + .modifiedByUser(MODIFIED_BY_USER) .modificationTime(MODIFICATION_TIME) .title(TITLE) .build(); - given(repository.findOne(ID)).willReturn(Optional.of(found)); + given(repository.findOne(ID)).willReturn(Optional.of(deleted)); + } + @Test + public void shouldDeleteFoundTodoEntry() { + service.delete(ID); + + verify(repository, times(1)).delete(deleted); + } + + @Test + public void shouldReturnTheInformationOfDeletedTodoEntry() { TodoDTO deleted = service.delete(ID); assertThatTodoDTO(deleted) @@ -160,7 +170,9 @@ public void shouldReturnTheInformationOfDeletedTodoEntry() { .hasId(ID) .hasTitle(TITLE) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } } } @@ -169,10 +181,13 @@ public class FindAll { public class WhenNoTodoEntryAreFound { - @Test - public void shouldReturnEmptyList() { + @Before + public void returnNoTodoEntries() { given(repository.findAll()).willReturn(new ArrayList<>()); + } + @Test + public void shouldReturnEmptyList() { List todoEntries = service.findAll(); assertThat(todoEntries).isEmpty(); @@ -181,29 +196,40 @@ public void shouldReturnEmptyList() { public class WhenOneTodoEntryIsFound { - @Test - public void shouldReturnInformationOfFoundTodoEntry() { + @Before + public void returnOneTodoEntry() { Todo found = new TodoBuilder() .id(ID) + .createdByUser(CREATED_BY_USER) .creationTime(CREATION_TIME) .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) .modificationTime(MODIFICATION_TIME) .title(TITLE) .build(); given(repository.findAll()).willReturn(Arrays.asList(found)); + } + @Test + public void shouldReturnOneTodoEntry() { List todoEntries = service.findAll(); assertThat(todoEntries).hasSize(1); - TodoDTO todoEntry = todoEntries.iterator().next(); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO todoEntry = service.findAll().get(0); assertThatTodoDTO(todoEntry) .hasId(ID) .hasTitle(TITLE) .hasDescription(DESCRIPTION) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } } } @@ -212,11 +238,14 @@ public class FindOne { public class WhenTodoEntryIsNotFound { - @Test - public void shouldThrowExceptionWithCorrectId() { + @Before + public void returnNoTodoEntry() { given(repository.findOne(ID)).willReturn(Optional.empty()); + } - Throwable thrown = thrown(() -> service.findById(ID)); + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.findById(ID)); assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); @@ -227,18 +256,23 @@ public void shouldThrowExceptionWithCorrectId() { public class WhenTodoEntryIsFound { - @Test - public void shouldReturnInformationOfFoundTodoEntry() { + @Before + public void returnFoundTodoEntry() { Todo found = new TodoBuilder() .id(ID) + .createdByUser(CREATED_BY_USER) .creationTime(CREATION_TIME) .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) .modificationTime(MODIFICATION_TIME) .title(TITLE) .build(); given(repository.findOne(ID)).willReturn(Optional.of(found)); + } + @Test + public void shouldReturnInformationOfFoundTodoEntry() { TodoDTO returned = service.findById(ID); assertThatTodoDTO(returned) @@ -246,7 +280,9 @@ public void shouldReturnInformationOfFoundTodoEntry() { .hasId(ID) .hasTitle(TITLE) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } } } @@ -255,15 +291,18 @@ public class Update { public class WhenTodoEntryIsNotFound { + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + @Test public void shouldThrowExceptionWithCorrectId() { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .id(ID) .build(); - given(repository.findOne(ID)).willReturn(Optional.empty()); - - Throwable thrown = thrown(() -> service.update(updatedTodoEntry)); + Throwable thrown = catchThrowable(() -> service.update(updatedTodoEntry)); assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); @@ -274,55 +313,54 @@ public void shouldThrowExceptionWithCorrectId() { public class WhenTodoEntryIsFound { - @Test - public void shouldUpdateTitleAndDescription() { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(ID) - .description(UPDATED_DESCRIPTION) - .title(UPDATED_TITLE) - .build(); + private Todo updated; - Todo updated = new TodoBuilder() + @Before + public void returnUpdatedTodoEntry() { + updated = new TodoBuilder() + .createdByUser(CREATED_BY_USER) .creationTime(CREATION_TIME) .description(DESCRIPTION) .id(ID) + .modifiedByUser(MODIFIED_BY_USER) .modificationTime(MODIFICATION_TIME) .title(TITLE) .build(); given(repository.findOne(ID)).willReturn(Optional.of(updated)); + } + + @Test + public void shouldUpdateTitleAndDescription() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); service.update(updatedTodoEntry); assertThatTodoEntry(updated) - .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .hasDescription(UPDATED_DESCRIPTION) + .hasTitle(UPDATED_TITLE); } @Test - public void shouldNotUpdateIdOrTimestamps() { + public void shouldNotUpdateIdOrAuditInformation() { TodoDTO updatedTodoEntry = new TodoDTOBuilder() .id(ID) .description(UPDATED_DESCRIPTION) .title(UPDATED_TITLE) .build(); - Todo updated = new TodoBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build(); - - given(repository.findOne(ID)).willReturn(Optional.of(updated)); - service.update(updatedTodoEntry); assertThatTodoEntry(updated) .hasId(ID) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } @Test @@ -333,16 +371,6 @@ public void shouldReturnInformationOfUpdatedTodoEntry() { .title(UPDATED_TITLE) .build(); - Todo updated = new TodoBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build(); - - given(repository.findOne(ID)).willReturn(Optional.of(updated)); - TodoDTO returnedTodoEntry = service.update(updatedTodoEntry); assertThatTodoDTO(returnedTodoEntry) @@ -350,7 +378,9 @@ public void shouldReturnInformationOfUpdatedTodoEntry() { .hasId(ID) .hasTitle(UPDATED_TITLE) .wasCreatedAt(CREATION_TIME) - .wasModifiedAt(MODIFICATION_TIME); + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); } } } diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java index 9dcd1ae..e88f27c 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java @@ -37,17 +37,25 @@ TodoAssert hasDescription(String expectedDescription) { return this; } - TodoAssert hasNoCreationTime() { + TodoAssert hasNoCreationAuditFieldValues() { isNotNull(); ZonedDateTime actualCreationTime = actual.getCreationTime(); assertThat(actualCreationTime) .overridingErrorMessage( - "Expected creation time to be but was <%s>", + "Expected creationTime to be but was <%s>", actualCreationTime ) .isNull(); + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + return this; } @@ -87,13 +95,21 @@ TodoAssert hasNoId() { return this; } - TodoAssert hasNoModificationTime() { + TodoAssert hasNoModificationAuditFieldValues() { isNotNull(); ZonedDateTime actualModificationTime = actual.getModificationTime(); assertThat(actualModificationTime) .overridingErrorMessage( - "Expected modification time to be but was <%s>.", + "Expected modificationTime to be but was <%s>.", + actualModificationTime + ) + .isNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>", actualModificationTime ) .isNull(); @@ -133,6 +149,21 @@ public TodoAssert wasCreatedAt(String creationTime) { return this; } + public TodoAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + public TodoAssert wasModifiedAt(String modificationTime) { isNotNull(); @@ -149,4 +180,19 @@ public TodoAssert wasModifiedAt(String modificationTime) { return this; } + + public TodoAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } } diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java index 1f4ca3f..90ee955 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java @@ -10,8 +10,10 @@ class TodoBuilder { private Long id; + private String createdByUser; private ZonedDateTime creationTime; private String description; + private String modifiedByUser; private ZonedDateTime modificationTime; private String title = "NOT_IMPORTANT"; @@ -22,6 +24,11 @@ TodoBuilder id(Long id) { return this; } + TodoBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + TodoBuilder creationTime(String creationTime) { this.creationTime = TestUtil.parseDateTime(creationTime); return this; @@ -32,6 +39,11 @@ TodoBuilder description(String description) { return this; } + TodoBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + TodoBuilder modificationTime(String modificationTime) { this.modificationTime = TestUtil.parseDateTime(modificationTime); return this; @@ -48,8 +60,10 @@ Todo build() { .description(description) .build(); + ReflectionTestUtils.setField(build, "createdByUser", createdByUser); ReflectionTestUtils.setField(build, "creationTime", creationTime); ReflectionTestUtils.setField(build, "id", id); + ReflectionTestUtils.setField(build, "modifiedByUser", modifiedByUser); ReflectionTestUtils.setField(build, "modificationTime", modificationTime); return build; diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java index 29944e7..462d90d 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java @@ -49,12 +49,20 @@ public TodoDTOAssert hasId(Long expectedId) { return this; } - public TodoDTOAssert hasNoCreationTime() { + public TodoDTOAssert hasNoCreationAuditFieldValues() { isNotNull(); + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + ZonedDateTime actualCreationTime = actual.getCreationTime(); assertThat(actualCreationTime) - .overridingErrorMessage("Expected creation time to be but was <%s>", actualCreationTime) + .overridingErrorMessage("Expected creationTime to be but was <%s>", actualCreationTime) .isNull(); return this; @@ -71,9 +79,17 @@ public TodoDTOAssert hasNoId() { return this; } - public TodoDTOAssert hasNoModificationTime() { + public TodoDTOAssert hasNoModificationAuditFieldValues() { isNotNull(); + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be but was <%s>", + actualModifiedByUser + ) + .isNull(); + ZonedDateTime actualModificationTime = actual.getModificationTime(); assertThat(actualModificationTime) .overridingErrorMessage("Expected modification time to be but was <%d>", actualModificationTime) @@ -114,6 +130,21 @@ public TodoDTOAssert wasCreatedAt(String creationTime) { return this; } + public TodoDTOAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + public TodoDTOAssert wasModifiedAt(String modificationTime) { isNotNull(); @@ -130,4 +161,19 @@ public TodoDTOAssert wasModifiedAt(String modificationTime) { return this; } + + public TodoDTOAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } } diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java index 594fed5..e0b5505 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java @@ -7,14 +7,21 @@ */ public class TodoDTOBuilder { + private String createdByUser; private ZonedDateTime creationTime; private String description; private Long id; + private String modifiedByUser; private ZonedDateTime modificationTime; private String title = "NOT_IMPORTANT"; public TodoDTOBuilder() {} + public TodoDTOBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + public TodoDTOBuilder creationTime(String creationTime) { this.creationTime = TestUtil.parseDateTime(creationTime); return this; @@ -30,6 +37,11 @@ public TodoDTOBuilder id(Long id) { return this; } + public TodoDTOBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + public TodoDTOBuilder modificationTime(String modificationTime) { this.modificationTime = TestUtil.parseDateTime(modificationTime); return this; @@ -43,9 +55,11 @@ public TodoDTOBuilder title(String title) { public TodoDTO build() { TodoDTO build = new TodoDTO(); + build.setCreatedByUser(createdByUser); build.setCreationTime(creationTime); build.setDescription(description); build.setId(id); + build.setModifiedByUser(modifiedByUser); build.setModificationTime(modificationTime); build.setTitle(title); diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java index 8a7783b..c5ff69d 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java @@ -1,13 +1,11 @@ package net.petrikainulainen.springdata.jpa.todo; import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.time.ZonedDateTime; - import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; -import static org.assertj.core.api.Assertions.assertThat; /** * @author Petri Kainulainen @@ -28,39 +26,59 @@ public class Build { public class WhenTitleIsInvalid { - @Test(expected = NullPointerException.class) - public void shouldThrowExceptionWhenTitleIsNull() { - Todo.getBuilder() - .title(null) - .description(DESCRIPTION) - .build(); + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(null) + .description(DESCRIPTION) + .build(); + } } - @Test(expected = IllegalArgumentException.class) - public void shouldThrowExceptionWhenTitleIsEmpty() { - Todo.getBuilder() - .title("") - .description(DESCRIPTION) - .build(); + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title("") + .description(DESCRIPTION) + .build(); + } } - @Test(expected = IllegalArgumentException.class) - public void shouldThrowExceptionWhenTitleIsTooLong() { - String tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + public class WhenTitleIsTooLong { - Todo.getBuilder() - .title(tooLongTitle) - .description(DESCRIPTION) - .build(); + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(tooLongTitle) + .description(DESCRIPTION) + .build(); + } } } public class WhenDescriptionIsTooLong { + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + } + + @Test(expected = IllegalArgumentException.class) public void shouldThrowException() { - String tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); - Todo.getBuilder() .title(TITLE) .description(tooLongDescription) @@ -71,118 +89,176 @@ public void shouldThrowException() { public class WhenTitleAndDescriptionAreValid { @Test - public void shouldCreateNewObjectWhenMaxLengthTitleIsGiven() { - String maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + public void shouldNotSetId() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoId(); + } + @Test + public void shouldNotSetCreationAuditFieldValues() { Todo build = Todo.getBuilder() - .title(maxLengthTitle) + .title(TITLE) .description(DESCRIPTION) .build(); assertThatTodoEntry(build) - .hasTitle(maxLengthTitle) - .hasDescription(DESCRIPTION) - .hasNoCreationTime() - .hasNoId() - .hasNoModificationTime(); + .hasNoCreationAuditFieldValues(); } @Test - public void shouldCreateNewObjectWhenMaxLengthDescriptionIsGiven() { - String maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + public void shouldNotSetModificationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoModificationAuditFieldValues(); + } + @Test + public void shouldSetDescription() { Todo build = Todo.getBuilder() .title(TITLE) - .description(maxLengthDescription) + .description(DESCRIPTION) .build(); assertThatTodoEntry(build) - .hasTitle(TITLE) - .hasDescription(maxLengthDescription) - .hasNoId() - .hasNoCreationTime() - .hasNoModificationTime(); + .hasDescription(DESCRIPTION); } @Test - public void shouldCreateNewObjectWhenNoDescriptionIsGiven() { + public void shouldSetTitle() { Todo build = Todo.getBuilder() .title(TITLE) + .description(DESCRIPTION) .build(); assertThatTodoEntry(build) - .hasTitle(TITLE) - .hasNoId() - .hasNoCreationTime() - .hasNoDescription() - .hasNoModificationTime(); + .hasTitle(TITLE); } - } - } - public class PrePersist { + public class WhenMaxLengthTitleIsGiven { - @Test - public void shouldUseSameTimeAsCreationTimeAndModificationTime() { - Todo newTodoEntry = Todo.getBuilder() - .title(TITLE) - .build(); + private String maxLengthTitle; - newTodoEntry.prePersist(); + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldCreateNewObjectAndSetTitle() { + Todo build = Todo.getBuilder() + .title(maxLengthTitle) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(maxLengthTitle); + } + } - ZonedDateTime creationTime = newTodoEntry.getCreationTime(); - ZonedDateTime modificationTime = newTodoEntry.getModificationTime(); + public class WhenMaxLengthDescriptionIsGiven { - assertThat(creationTime).isNotNull(); - assertThat(modificationTime).isNotNull(); - assertThat(creationTime).isEqualTo(modificationTime); + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + + } + + @Test + public void shouldCreateNewObjectAndSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(maxLengthDescription) + .build(); + + assertThatTodoEntry(build) + .hasDescription(maxLengthDescription); + } + } + + public class WhenNoDescriptionIsGiven { + + @Test + public void shouldCreateNewObjectWithoutDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .build(); + + assertThatTodoEntry(build) + .hasNoDescription(); + } + } } } public class Update { + private Todo updated; + + @Before + public void createUpdatedTodoEntry() { + updated = Todo.getBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + } + public class WhenNewTitleIsInvalid { - @Test(expected = NullPointerException.class) - public void shouldThrowExceptionWhenNewTitleIsNull() { - Todo updated = Todo.getBuilder() - .title(TITLE) - .build(); + public class WhenTitleIsNull { - updated.update(null, UPDATED_DESCRIPTION); + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + updated.update(null, UPDATED_DESCRIPTION); + } } - @Test(expected = IllegalArgumentException.class) - public void shouldThrowExceptionWhenNewTitleIsEmpty() { - Todo updated = Todo.getBuilder() - .title(TITLE) - .build(); + public class WhenTitleIsEmpty { - updated.update("", UPDATED_DESCRIPTION); + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update("", UPDATED_DESCRIPTION); + } } - @Test(expected = IllegalArgumentException.class) - public void shouldThrowExceptionWhenNewTitleIsTooLong() { - String tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + public class WhenTitleIsTooLong { - Todo updated = Todo.getBuilder() - .title(TITLE) - .build(); + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } - updated.update(tooLongTitle, UPDATED_DESCRIPTION); + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(tooLongTitle, UPDATED_DESCRIPTION); + } } } public class WhenNewDescriptionIsTooLong { - @Test(expected = IllegalArgumentException.class) - public void shouldThrowException() { - String tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); - Todo updated = Todo.getBuilder() - .description(DESCRIPTION) - .title(TITLE) - .build(); + private String tooLongDescription; + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { updated.update(UPDATED_TITLE, tooLongDescription); } } @@ -191,14 +267,15 @@ public class WhenNewTitleAndNewDescriptionAreValid { public class WhenMaxLengthTitleAndNewDescriptionAreGiven { - @Test - public void shouldUpdateTitle() { - String maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + private String maxLengthTitle; - Todo updated = Todo.getBuilder() - .title(TITLE) - .build(); + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + @Test + public void shouldUpdateTitle() { updated.update(maxLengthTitle, UPDATED_DESCRIPTION); assertThatTodoEntry(updated) @@ -207,12 +284,6 @@ public void shouldUpdateTitle() { @Test public void shouldUpdateDescription() { - String maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); - - Todo updated = Todo.getBuilder() - .title(TITLE) - .build(); - updated.update(maxLengthTitle, UPDATED_DESCRIPTION); assertThatTodoEntry(updated) @@ -224,11 +295,6 @@ public class WhenNewTitleIsGivenAndNewDescriptionIsNull { @Test public void shouldUpdateTitle() { - Todo updated = Todo.getBuilder() - .description(DESCRIPTION) - .title(TITLE) - .build(); - updated.update(UPDATED_TITLE, null); assertThatTodoEntry(updated) @@ -237,11 +303,6 @@ public void shouldUpdateTitle() { @Test public void shouldRemoveDescription() { - Todo updated = Todo.getBuilder() - .description(DESCRIPTION) - .title(TITLE) - .build(); - updated.update(UPDATED_TITLE, null); assertThatTodoEntry(updated) @@ -249,17 +310,17 @@ public void shouldRemoveDescription() { } } - public class WhenNewTitleIsGivenAndMaxLengthDescriptionAreGiven { + public class WhenNewTitleAndMaxLengthDescriptionAreGiven { - @Test - public void shouldUpdateTitle() { - String maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + private String maxLengthDescription; - Todo updated = Todo.getBuilder() - .description(DESCRIPTION) - .title(TITLE) - .build(); + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + } + @Test + public void shouldUpdateTitle() { updated.update(UPDATED_TITLE, maxLengthDescription); assertThatTodoEntry(updated) @@ -268,13 +329,6 @@ public void shouldUpdateTitle() { @Test public void shouldUpdateDescription() { - String maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); - - Todo updated = Todo.getBuilder() - .description(DESCRIPTION) - .title(TITLE) - .build(); - updated.update(UPDATED_TITLE, maxLengthDescription); assertThatTodoEntry(updated) diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java index fd901ad..a0dd3eb 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java @@ -1,5 +1,6 @@ package net.petrikainulainen.springdata.jpa.web; +import com.nitorcreations.junit.runners.NestedRunner; import net.petrikainulainen.springdata.jpa.todo.TestUtil; import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; import net.petrikainulainen.springdata.jpa.todo.TodoDTO; @@ -8,9 +9,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.support.StaticMessageSource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -19,12 +17,14 @@ import java.util.Arrays; import java.util.Locale; +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -39,10 +39,11 @@ /** * @author Petri Kainulainen */ -@RunWith(MockitoJUnitRunner.class) +@RunWith(NestedRunner.class) public class TodoControllerTest { private static final Locale CURRENT_LOCALE = Locale.US; + private static final String CREATED_BY_USER = "createdByUser"; private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; private static final String DESCRIPTION = "description"; @@ -52,18 +53,20 @@ public class TodoControllerTest { private static final String ERROR_MESSAGE_KEY_TOO_LONG_TITLE = "Size.todoDTO.title"; private static final Long ID = 1L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; private static final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; private static final String TITLE = "title"; private MockMvc mockMvc; - @Mock private TodoCrudService crudService; private StaticMessageSource messageSource; @Before public void setUp() { + crudService = mock(TodoCrudService.class); + messageSource = new StaticMessageSource(); messageSource.setUseCodeAsDefaultMessage(true); @@ -75,519 +78,614 @@ public void setUp() { .build(); } - @Test - public void create_EmptyTodoEntry_ShouldReturnResponseStatusBadRequest() throws Exception { - TodoDTO emptyTodoEntry = new TodoDTO(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) - ) - .andExpect(status().isBadRequest()); - } - - @Test - public void create_EmptyTodoEntry_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { - TodoDTO emptyTodoEntry = new TodoDTO(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) - .andExpect(jsonPath("$.fieldErrors", hasSize(1))) - .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) - .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); - } - - @Test - public void create_EmptyTodoEntry_ShouldNotCreateNewTodoEntry() throws Exception { - TodoDTO emptyTodoEntry = new TodoDTO(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) - ); - - verifyZeroInteractions(crudService); - } - - @Test - public void create_TooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .title(tooLongTitle) - .build(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ) - .andExpect(status().isBadRequest()); - } - - @Test - public void create_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .title(tooLongTitle) - .build(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) - .andExpect(jsonPath("$.fieldErrors", hasSize(2))) - .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( - WebTestConstants.FIELD_NAME_DESCRIPTION, - WebTestConstants.FIELD_NAME_TITLE - ))) - .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( - ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, - ERROR_MESSAGE_KEY_TOO_LONG_TITLE - ))); - } - - @Test - public void create_TooLongTitleAndDescription_ShouldNotCreateNewTodoEntry() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .title(tooLongTitle) - .build(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ); - - verifyZeroInteractions(crudService); - } - - @Test - public void create_MaxLengthTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .title(maxLengthTitle) - .build(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ) - .andExpect(status().isCreated()); - } - - @Test - public void create_MaxLengthTitleAndDescription_ShouldReturnCreatedTodoEntryAsJson() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .title(maxLengthTitle) - .build(); - - TodoDTO created = new TodoDTOBuilder() - .creationTime(CREATION_TIME) - .description(maxLengthDescription) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(maxLengthTitle) - .build(); - given(crudService.create(isA(TodoDTO.class))).willReturn(created); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) - .andExpect(jsonPath("$.description", is(maxLengthDescription))) - .andExpect(jsonPath("$.id", is(ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(maxLengthTitle))); - } - - @Test - public void create_MaxLengthTitleAndDescription_ShouldCreateNewTodoEntryWithCorrectInformation() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO newTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .title(maxLengthTitle) - .build(); - - mockMvc.perform(post("/api/todo") - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) - ); - - ArgumentCaptor createdTodoEntryArgument = ArgumentCaptor.forClass(TodoDTO.class); - verify(crudService, times(1)).create(createdTodoEntryArgument.capture()); - - TodoDTO created = createdTodoEntryArgument.getValue(); - - assertThatTodoDTO(created) - .hasDescription(maxLengthDescription) - .hasTitle(maxLengthTitle) - .hasNoCreationTime() - .hasNoId() - .hasNoModificationTime(); - } - - @Test - public void delete_TodoEntryNotFound_ShouldReturnResponseStatusNotFound() throws Exception { - given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(delete("/api/todo/{id}", ID)) - .andExpect(status().isNotFound()); - } - - @Test - public void delete_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { - given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(delete("/api/todo/{id}", ID)) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) - .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); - } - - @Test - public void delete_TodoEntryFound_ShouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { - TodoDTO deleted = new TodoDTOBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build(); - - given(crudService.delete(ID)).willReturn(deleted); - - mockMvc.perform(delete("/api/todo/{id}", ID)) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) - .andExpect(jsonPath("$.description", is(DESCRIPTION))) - .andExpect(jsonPath("$.id", is(ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(TITLE))); - } - - @Test - public void findAll_ShouldReturnResponseStatusOk() throws Exception { - mockMvc.perform(get("/api/todo")) - .andExpect(status().isOk()); - } - - @Test - public void findAll_NoTodoEntriesFound_ShouldReturnEmptyListAsJson() throws Exception { - given(crudService.findAll()).willReturn(new ArrayList<>()); - - mockMvc.perform(get("/api/todo")) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$", hasSize(0))); - } - - @Test - public void findAll_OneTodoEntryFound_ShouldReturnOneTodoEntryAsJson() throws Exception { - TodoDTO found = new TodoDTOBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build(); - - given(crudService.findAll()).willReturn(Arrays.asList(found)); - - mockMvc.perform(get("/api/todo")) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) - .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) - .andExpect(jsonPath("$[0].id", is(ID.intValue()))) - .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) - .andExpect(jsonPath("$[0].title", is(TITLE))); - } - - @Test - public void findById_TodoEntryNotFound_ShouldReturnResponseStatusNotFound() throws Exception { - given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(get("/api/todo/{id}", ID)) - .andExpect(status().isNotFound()); - } - - @Test - public void findId_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { - given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(get("/api/todo/{id}", ID)) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) - .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + public class Create { + + public class WhenTodoEntryIsNotValid { + + public class WhenTodoEntryIsEmpty { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + private TodoDTO newTodoEntry; + + @Before + public void createInputAndReturnNewTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + newTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .title(maxLengthTitle) + .build(); + + TodoDTO created = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.create(isA(TodoDTO.class))).willReturn(created); + } + + @Test + public void shouldReturnResponseStatusCreated() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isCreated()); + } + + @Test + public void shouldReturnCreatedTodoEntryAsJson() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldCreateNewTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verify(crudService, times(1)).create( + assertArg(created -> assertThatTodoDTO(created) + .hasDescription(maxLengthDescription) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoId() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } } - @Test - public void findById_TodoEntryFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { - TodoDTO found = new TodoDTOBuilder() - .creationTime(CREATION_TIME) - .description(DESCRIPTION) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(TITLE) - .build(); - - given(crudService.findById(ID)).willReturn(found); - - mockMvc.perform(get("/api/todo/{id}", ID)) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) - .andExpect(jsonPath("$.description", is(DESCRIPTION))) - .andExpect(jsonPath("$.id", is(ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(TITLE))); + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwNotFoundException() { + given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnDeletedTodoEntry() { + TodoDTO deleted = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.delete(ID)).willReturn(deleted); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } } - @Test - public void update_TodoEntryNotFound_ShouldReturnResponseStatusNotFound() throws Exception { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(ID) - .build(); - - given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(status().isNotFound()); + public class FindAll { + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnNoTodoEntries() { + given(crudService.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } } - @Test - public void update_TodoEntryNotFound_ShouldReturnErrorMessageAsJson() throws Exception { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .id(ID) - .build(); - - given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) - .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); - } - - @Test - public void update_TitleAndDescriptionAreMissing_ShouldReturnResponseStatusBadRequest() throws Exception { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(null) - .id(ID) - .title(null) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(status().isBadRequest()); - } - - @Test - public void update_TitleAndDescriptionAreMissing_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(null) - .id(ID) - .title(null) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) - .andExpect(jsonPath("$.fieldErrors", hasSize(1))) - .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) - .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); - } - - @Test - public void update_TitleAndDescriptionAreMissing_ShouldNotUpdateTodoEntry() throws Exception { - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(null) - .id(ID) - .title(null) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ); - - verifyZeroInteractions(crudService); + public class FindById { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findById(ID)).willReturn(found); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } } - @Test - public void update_TooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .id(ID) - .title(tooLongTitle) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(status().isBadRequest()); - } - - @Test - public void update_TooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .id(ID) - .title(tooLongTitle) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) - .andExpect(jsonPath("$.fieldErrors", hasSize(2))) - .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( - WebTestConstants.FIELD_NAME_DESCRIPTION, - WebTestConstants.FIELD_NAME_TITLE - ))) - .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( - ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, - ERROR_MESSAGE_KEY_TOO_LONG_TITLE - ))); - } - - @Test - public void update_TooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { - String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); - String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(tooLongDescription) - .id(ID) - .title(tooLongTitle) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ); - - verifyZeroInteractions(crudService); - } - - @Test - public void update_MaxLengthTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .id(ID) - .title(maxLengthTitle) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(status().isOk()); - } - - @Test - public void update_MaxLengthTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .id(ID) - .title(maxLengthTitle) - .build(); - - TodoDTO updated = new TodoDTOBuilder() - .creationTime(CREATION_TIME) - .description(maxLengthDescription) - .id(ID) - .modificationTime(MODIFICATION_TIME) - .title(maxLengthTitle) - .build(); - given(crudService.update(isA(TodoDTO.class))).willReturn(updated); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ) - .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) - .andExpect(jsonPath("$.description", is(maxLengthDescription))) - .andExpect(jsonPath("$.id", is(ID.intValue()))) - .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) - .andExpect(jsonPath("$.title", is(maxLengthTitle))); - } - - @Test - public void update_MaxLengthTitleAndDescription_ShouldUpdateTodoEntryWithCorrectInformation() throws Exception { - String maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); - String maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); - - TodoDTO updatedTodoEntry = new TodoDTOBuilder() - .description(maxLengthDescription) - .id(ID) - .title(maxLengthTitle) - .build(); - - mockMvc.perform(put("/api/todo/{id}", ID) - .contentType(WebTestConstants.APPLICATION_JSON_UTF8) - .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) - ); - - ArgumentCaptor updatedArgument = ArgumentCaptor.forClass(TodoDTO.class); - verify(crudService, times(1)).update(updatedArgument.capture()); - - TodoDTO updated = updatedArgument.getValue(); - assertThatTodoDTO(updated) - .hasDescription(maxLengthDescription) - .hasId(ID) - .hasTitle(maxLengthTitle) - .hasNoCreationTime() - .hasNoModificationTime(); + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + public class WhenTodoEntryIsNotValid { + + public class WhenTitleAndDescriptionAreMissing { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + TodoDTO updatedTodoEntry; + + @Before + public void createInputAndReturnUpdatedTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + updatedTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .id(ID) + .title(maxLengthTitle) + .build(); + + TodoDTO updated = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.update(isA(TodoDTO.class))).willReturn(updated); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldUpdateTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verify(crudService, times(1)).update( + assertArg(updated -> assertThatTodoDTO(updated) + .hasDescription(maxLengthDescription) + .hasId(ID) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } } } diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java new file mode 100644 index 0000000..98c060d --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java @@ -0,0 +1,250 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoSearchControllerTest { + + private MockMvc mockMvc; + + private TodoSearchService searchService; + + @Before + public void setUp() { + searchService = mock(TodoSearchService.class); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoSearchController(searchService)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setCustomArgumentResolvers(WebTestConfig.pageRequestArgumentResolver()) + .build(); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final String PAGE_NUMBER_STRING = "1"; + private final int PAGE_SIZE = 5; + private final String PAGE_SIZE_STRING = "5"; + private final String SEARCH_TERM = "itl"; + + private Pageable pageRequest; + + @Before + public void setUp() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnPageNumberAndPageSizeAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.number", is(PAGE_NUMBER))) + .andExpect(jsonPath("$.size", is(PAGE_SIZE))); + } + + @Test + public void shouldReturnSortInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.sort[*].direction[0]", is(WebTestConstants.SORT_DIRECTION_ASC))) + .andExpect(jsonPath("$.sort[*].property[0]", is(WebTestConstants.FIELD_NAME_TITLE))); + } + + @Test + public void shouldReturnPTotalElementInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + @Test + public void shouldPassSearchTermForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class)); + } + + @Test + public void shouldPassPageSizeAndNumberForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> { + assertThat(pageRequest.getPageNumber()).isEqualTo(PAGE_NUMBER); + assertThat(pageRequest.getPageSize()).isEqualTo(PAGE_SIZE); + } + )); + } + + @Test + public void shouldPassSortForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> assertThat( + pageRequest.getSort().getOrderFor(WebTestConstants.FIELD_NAME_TITLE).getDirection()) + .isEqualTo(Sort.Direction.ASC) + ) + ); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnEmptyPage() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements", is(0))); + } + } + + + public class WhenOneTodoEntryIsFound { + + private final Long ID= 1L; + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page pageWithOneTodoEntry = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(pageWithOneTodoEntry); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$.content[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TITLE))); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java index 0669579..8578c38 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java @@ -4,7 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import net.petrikainulainen.springdata.jpa.web.error.RestErrorHandler; import org.springframework.context.MessageSource; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.method.HandlerMethod; @@ -65,11 +68,11 @@ static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() { } /** - * This method ensures that the {@link net.petrikainulainen.springdata.jpa.web.RestErrorHandler} class + * This method ensures that the {@link RestErrorHandler} class * is used to handle the exceptions thrown by the tested controller. I borrowed this idea from * this StackOverflow answer. * - * @return an error handler component that delegates relevant exceptions forward to the {@link net.petrikainulainen.springdata.jpa.web.RestErrorHandler} class. + * @return an error handler component that delegates relevant exceptions forward to the {@link RestErrorHandler} class. */ static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) { final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() { @@ -88,6 +91,26 @@ protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerM return exceptionResolver; } + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Sort} objects by using the request params of the + * incoming request. + * @return + */ + static SortHandlerMethodArgumentResolver sortArgumentResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Pageable} objects by using the request params of the + * incoming request. + * @return + */ + static PageableHandlerMethodArgumentResolver pageRequestArgumentResolver() { + return new PageableHandlerMethodArgumentResolver(sortArgumentResolver()); + } + /** * This method creates a validator object that adds support for bean validation API 1.0 and 1.1. * diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java index fabd323..c50f7df 100644 --- a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java @@ -1,3 +1,4 @@ + package net.petrikainulainen.springdata.jpa.web; import org.springframework.http.MediaType; @@ -7,9 +8,9 @@ /** * @author Petri Kainulainen */ -final class WebTestConstants { +public final class WebTestConstants { - static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8") ); @@ -23,8 +24,15 @@ final class WebTestConstants { static final int MAX_LENGTH_DESCRIPTION = 500; static final int MAX_LENGTH_TITLE = 100; + static final String REQUEST_PARAM_PAGE_NUMBER = "page"; + static final String REQUEST_PARAM_PAGE_SIZE = "size"; + static final String REQUEST_PARAM_SEARCH_TERM = "searchTerm"; + static final String REQUEST_PARAM_SORT = "sort"; + + static final String SORT_DIRECTION_ASC = "ASC"; + /** * Prevents instantiation. */ private WebTestConstants() {} -} +} \ No newline at end of file diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java new file mode 100644 index 0000000..528a552 --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.ErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ErrorDTOTest { + + private static final String CODE = "code"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenCodeIsInvalid { + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenCodeIsNull() { + new ErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenCodeIsEmpty() { + new ErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new ErrorDTO(CODE, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new ErrorDTO(CODE, ""); + } + } + + public class WhenCodeAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetCode() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getCode()).isEqualTo(CODE); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java new file mode 100644 index 0000000..25fc6bf --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java @@ -0,0 +1,64 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class FieldErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenFieldIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenFieldIsNull() { + new FieldErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenFieldIsEmpty() { + new FieldErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new FieldErrorDTO(FIELD, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new FieldErrorDTO(FIELD, ""); + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetField() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java new file mode 100644 index 0000000..5ff8f34 --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java @@ -0,0 +1,242 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.MethodParameter; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.List; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RestErrorHandlerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + + private static final Long TODO_ID = 99L; + + private MessageSource messageSource; + + private RestErrorHandler errorHandler; + + @Before + public void setUp() { + messageSource = mock(MessageSource.class); + this.errorHandler = new RestErrorHandler(messageSource); + } + + public class HandleTodoEntryNotFound { + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + + private static final String ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 99"; + + @Before + public void returnErrorMessageNotFound() { + given(messageSource.getMessage( + isA(MessageSourceResolvable.class), + isA(Locale.class)) + ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldFindErrorMessageByUsingCurrentLocale() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage(isA(MessageSourceResolvable.class), eq(CURRENT_LOCALE)); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectId() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getArguments()) + .containsOnly(TODO_ID) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectMessageCode() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getCodes()) + .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldReturnErrorThatHasCorrectErrorCode() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getCode()).isEqualTo(ERROR_CODE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldReturnErrorThatHasCorrectMessage() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getMessage()).isEqualTo(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + } + + public class HandleValidationErrors { + + private static final String ERROR_CODE_VALIDATION_ERROR = "BAD_REQUEST"; + private static final String ERROR_MESSAGE_VALIDATION_ERROR = "validationError"; + + private static final String FIELD_DEFAULT_MESSAGE = "DefaultMessage"; + private static final String FIELD_WITH_VALIDATION_ERROR = "field"; + private static final String OBJECT_WITH_VALIDATION_ERROR = "todoDTO"; + + private static final String VALIDATION_ERROR_CODE_ACCURATE = "Error"; + private static final String VALIDATION_ERROR_CODE_LESS_ACCURATE = "Maybe"; + + public class WhenOneValidationErrorIsFound { + + public class WhenMessageIsFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(ERROR_MESSAGE_VALIDATION_ERROR); + } + } + + public class WhenMessageIsNotFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnDefaultErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasFieldErrorWithMostAccurateFieldErrorCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(VALIDATION_ERROR_CODE_ACCURATE); + } + } + } + + private MethodArgumentNotValidException createExceptionWithFieldErrors(FieldError... fieldErrors) { + BindingResult bindingResult = new BeanPropertyBindingResult(new TodoDTO(), OBJECT_WITH_VALIDATION_ERROR); + + for (FieldError fieldError: fieldErrors) { + bindingResult.addError(fieldError); + } + + return new MethodArgumentNotValidException(mock(MethodParameter.class), bindingResult); + } + + + private final class FieldErrorBuilder { + + private String defaultMessage; + private String[] errorCodes; + private String fieldName; + + private FieldErrorBuilder() {} + + private FieldErrorBuilder defaultMessage(String defaultMessage) { + this.defaultMessage = defaultMessage; + return this; + } + + private FieldErrorBuilder errorCodes(String... errorCodes) { + this.errorCodes = errorCodes; + return this; + } + + private FieldErrorBuilder fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + private FieldError build() { + return new FieldError(OBJECT_WITH_VALIDATION_ERROR, + fieldName, + null, + false, + errorCodes, + new Object[]{}, + defaultMessage + ); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java new file mode 100644 index 0000000..8ae069a --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import net.petrikainulainen.springdata.jpa.web.error.ValidationErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ValidationErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class AddFieldError { + + public class WhenFieldIsInvalid { + + public class WhenFieldIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(null, MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(null, MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenFieldIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError("", MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError("", MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenMessageIsInvalid { + + public class WhenMessageIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, null); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, null)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenMessageIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, ""); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, "")); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewFieldErrorAndSetField() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewFieldErrorAndSetMessage() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java new file mode 100644 index 0000000..1928461 --- /dev/null +++ b/query-methods/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class UserDTOTest { + + public class CreateNew { + + private final String ROLE_USER = UserRole.ROLE_USER.name(); + + public class WhenUsernameIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER); + new UserDTO("", authorities); + } + } + + public class WhenUserNameIsNotEmpty { + + private final String USERNAME = "username"; + + public class WhenUserHasNoGrantedAuthorities { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + new UserDTO(USERNAME, new ArrayList<>()); + } + } + + public class WhenUserHasOneGrantedAuthority { + + public class WhenGrantedAuthorityIsKnown { + + private Collection authorities; + + @Before + public void createKnownAuthority() { + authorities = createAuthorities(ROLE_USER); + } + + @Test + public void shouldSetUsername() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getUsername()).isEqualTo(USERNAME); + } + + @Test + public void shouldSetRole() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getRole()).isEqualTo(UserRole.ROLE_USER); + } + } + + public class WhenGrantedAuthorityIsUnknown { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities("UNKNOWN_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + + public class WhenUserHasMoreThanOneGrantedAuthority { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER, "ANOTHER_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + } + + private Collection createAuthorities(String... roles) { + List authorities = new ArrayList<>(); + + for (String role: roles) { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); + authorities.add(authority); + } + + return authorities; + } +} diff --git a/querydsl/.gitignore b/querydsl/.gitignore new file mode 100644 index 0000000..02895f1 --- /dev/null +++ b/querydsl/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.gradle +.idea +*.iml +build +h2db +target +node_modules +bower_components +build \ No newline at end of file diff --git a/tutorial-part-six/LICENSE b/querydsl/LICENSE similarity index 88% rename from tutorial-part-six/LICENSE rename to querydsl/LICENSE index b333aa5..642bfb3 100644 --- a/tutorial-part-six/LICENSE +++ b/querydsl/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011 Petri Kainulainen +Copyright 2014 Petri Kainulainen Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -10,4 +10,4 @@ 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. \ No newline at end of file +limitations under the License. diff --git a/querydsl/README.md b/querydsl/README.md new file mode 100644 index 0000000..4815837 --- /dev/null +++ b/querydsl/README.md @@ -0,0 +1,81 @@ +This blog post is the example application of the following blog posts: + +* [Spring Data JPA Tutorial: Creating Database Queries With Querydsl](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-five-querydsl/) + +You might also want to read the other parts of my Spring Data JPA Tutorial: + + +* [Spring Data JPA Tutorial: Getting the Required Dependencies](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-getting-the-required-dependencies/) +* [Spring Data JPA Tutorial: Configuration](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/) +* [Spring Data JPA Tutorial: CRUD](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/) +* [Spring Data JPA Tutorial: Introduction to Query Methods](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-introduction-to-query-methods/) +* [Spring Data JPA Tutorial: Creating Database Queries From Method Names](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-from-method-names/) +* [Spring Data JPA Tutorial: Creating Database Queries With the @Query Annotation](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-the-query-annotation/) +* [Spring Data JPA Tutorial: Creating Database Queries With Named Queries](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-creating-database-queries-with-named-queries/) +* [Spring Data JPA Tutorial: Creating Database Queries With the JPA Criteria API](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-four-jpa-criteria-queries/) +* [Spring Data JPA Tutorial: Auditing, Part One](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-one/) +* [Spring Data JPA Tutorial: Auditing, Part Two](http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-auditing-part-two/) + +**Note:** This application is still work in progress. + +Prerequisites +============= + +You need to install the following tools if you want to run this application: + +Backend +--------- + +* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* [Maven](http://maven.apache.org/) (the application is tested with Maven 3.2.1) + +Frontend +---------- + +* [Node.js](http://nodejs.org/) +* [NPM](https://www.npmjs.org/) +* [Bower](http://bower.io/) +* [Gulp](http://gulpjs.com/) + +You can install these tools by following these steps: + +1. Install Node.js by using a [downloaded binary](http://nodejs.org/download/) or a [package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). + You can also read this blog post: [How to install Node.js and NPM](http://blog.nodeknockout.com/post/65463770933/how-to-install-node-js-and-npm) + +2. Install Bower by using the following command: + + npm install -g bower + +3. Install Gulp by using the following command: + + npm install -g gulp + + +Running the Tests +================= + +You can run the unit tests by using the following command: + + mvn clean test -P dev + +You can run the integration tests by using the following command: + + mvn clean verify -P integration-test + +Running the Application +======================= + +You can run the application by using the following command: + + mvn clean jetty:run -P dev + +Credits +========= + +* Kyösti Herrala. The Gulp build script and its Maven integration are based on Kyösti's ideas. +* [Techniques for authentication in AngularJS applications](https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec) + +Known Issues +============ + +* If you refresh the login page, you aren't redirected away from it after successful login. \ No newline at end of file diff --git a/querydsl/frontend/.bowerrc b/querydsl/frontend/.bowerrc new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/querydsl/frontend/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file diff --git a/querydsl/frontend/.jshintrc b/querydsl/frontend/.jshintrc new file mode 100644 index 0000000..f648d46 --- /dev/null +++ b/querydsl/frontend/.jshintrc @@ -0,0 +1,33 @@ +{ + "globalstrict": true, + "browser": true, + "devel": true, + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "globals": { + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "angular": true, + "jQuery": true, + "_": true, + "$": true + } +} \ No newline at end of file diff --git a/querydsl/frontend/app/app.js b/querydsl/frontend/app/app.js new file mode 100644 index 0000000..074fd4f --- /dev/null +++ b/querydsl/frontend/app/app.js @@ -0,0 +1,98 @@ +'use strict'; + +var App = angular.module('app', [ + 'angular-logger', + 'http-auth-interceptor', + 'ngLocale', + 'ngCookies', + 'ngResource', + 'ngSanitize', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.router', + 'ui.utils', + 'angular-growl', + 'angularMoment', + 'angularUtils.directives.dirPagination', + 'spring-security-csrf-token-interceptor', + + //Partials + 'templates', + + //Account + 'app.account.config', 'app.account.directives', 'app.account.controllers', 'app.account.services', + + //Common + 'app.common.config', 'app.common.controllers', 'app.common.directives', 'app.common.services', + + //Todo + 'app.todo.controllers', 'app.todo.directives', 'app.todo.services', + + //Search + 'app.search.controllers', 'app.search.directives', 'app.search.services' + +]); + +App.run(['$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', 'authService', 'AuthenticationService', 'COMMON_EVENTS', + function ($log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser, authService, AuthenticationService, COMMON_EVENTS) { + + var logger = $log.getInstance('app'); + + //This function retries all requests that were failed because of + //the 401 response. + function listenAuthenticationEvents() { + var confirmLogin = function() { + authService.loginConfirmed(); + }; + + $rootScope.$on(AUTH_EVENTS.loginSuccess, confirmLogin); + + var viewLogInPage = function() { + logger.info('User is not authenticated. Rendering login view.'); + $state.go('todo.login'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthenticated, viewLogInPage); + + var viewTodoListPage = function() { + logger.info("User logged out. REndering todo list view."); + $state.go('todo.list', {}, {reload: true}); + }; + + $rootScope.$on(AUTH_EVENTS.logoutSuccess, viewTodoListPage); + + var viewForbiddenPage = function() { + logger.info('Permission was denied for user: %j', AuthenticatedUser); + $state.go('todo.forbidden'); + }; + + $rootScope.$on(AUTH_EVENTS.notAuthorized, viewForbiddenPage); + } + + function listenCommonEvents() { + + var view404Page = function() { + logger.info('Requested page was not found.'); + $state.go('todo.404'); + }; + + $rootScope.$on(COMMON_EVENTS.notFound, view404Page); + } + + //This function ensures that anonymous users cannot access states + //that marked as protected (i.e. the value of the authenticated + //property is set to true). + function secureProtectedStates() { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + logger.trace('Moving to state: %s', toState.name); + AuthenticationService.authorizeStateChange(event, toState, toParams); + }); + } + + $rootScope.currentUser = AuthenticatedUser; + + listenAuthenticationEvents(); + listenCommonEvents(); + secureProtectedStates(); + }]); + diff --git a/querydsl/frontend/app/assets/i18n/en.json b/querydsl/frontend/app/assets/i18n/en.json new file mode 100644 index 0000000..869176b --- /dev/null +++ b/querydsl/frontend/app/assets/i18n/en.json @@ -0,0 +1,101 @@ +{ + "app.title.label": "Spring Data JPA Tutorial - Query Methods", + "dialogs": { + "delete.dialog": { + "cancel.button.label": "Cancel", + "delete.button.label": "Delete", + "text": "Are you sure that you want to delete the todo entry with title: {{title}}?", + "title": "Delete todo entry?" + } + }, + "directives": { + "login.form": { + "login.button": "Login", + "login.failed": "Login failed!" + }, + "log.out.link.label": "Log Out", + "todo.form": { + "cancel.button": "Cancel", + "save.button": "Save" + } + }, + "footer.message": "Spring Data JPA example application by Petri Kainulainen", + "header.brand.label": "Spring Data JPA Tutorial", + "pages": { + "add.page": { + "title": "Add new todo entry", + "link.label": "Add new todo entry" + }, + "delete.link": "Delete", + "edit.page": { + "link.label": "Edit", + "title": "Edit todo entry" + }, + "forbidden.page": { + "text": "Permission denied.", + "title": "Forbidden" + }, + "not.found.page": { + "text": "The page that you were looking for was not found.", + "title": "Not Found" + }, + "list.page": { + "title": "Things to do", + "texts": { + "no.todo.entries.found": "Nothing to do (yet)." + } + }, + "login.page": { + "title": "Log In" + }, + "search.results.page": { + "texts": { + "no.todo.entries.found": "No todo entries was found with the given search term." + }, + "title": "Search Results" + }, + "view.page": { + "title": "View Todo Entry" + } + }, + "login": { + "help": "Log in by using username: 'user' and password: 'password'", + "username": "Username", + "username.placeholder": "Enter username", + "password": "Password", + "password.placeholder": "Enter password" + }, + "search": { + "term.field.placeholder": "Search", + "missing.characters.text": "{{missingCharCount}} characters missing" + }, + "todo": { + "created.by.prefix": "by", + "creation.time": "Created at", + "description": "Description", + "description.placeholder": "Enter description", + "messages": { + "description.maxLength": "Description cannot be longer than 500 characters", + "title.maxLength": "Title cannot be longer than 100 characters", + "title.required": "Title is required" + }, + "modified.by.prefix": "by", + "modification.time": "Modified at", + "notifications": { + "add": { + "error": "Adding a new todo entry failed.", + "success": "A new todo entry was added." + }, + "delete": { + "error": "Deleting the todo entry failed.", + "success": "Deleted the todo entry." + }, + "edit": { + "error": "Updating the information of a todo entry failed.", + "success": "Updated the information of the todo entry." + } + }, + "title": "Title", + "title.placeholder": "Enter title" + } +} \ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/account/forbidden-view.html b/querydsl/frontend/app/assets/partials/account/forbidden-view.html new file mode 100644 index 0000000..c761f3e --- /dev/null +++ b/querydsl/frontend/app/assets/partials/account/forbidden-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/account/login-form-directive.html b/querydsl/frontend/app/assets/partials/account/login-form-directive.html new file mode 100644 index 0000000..d2f14aa --- /dev/null +++ b/querydsl/frontend/app/assets/partials/account/login-form-directive.html @@ -0,0 +1,36 @@ +
+ + +
+ +
+
+ : + +
+
+ : + +
+
+ +
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/account/login-view.html b/querydsl/frontend/app/assets/partials/account/login-view.html new file mode 100644 index 0000000..199d339 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/account/login-view.html @@ -0,0 +1,6 @@ +

+ +
+
+

+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/account/logout-link-directive.html b/querydsl/frontend/app/assets/partials/account/logout-link-directive.html new file mode 100644 index 0000000..4d9550a --- /dev/null +++ b/querydsl/frontend/app/assets/partials/account/logout-link-directive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/common/not-found-view.html b/querydsl/frontend/app/assets/partials/common/not-found-view.html new file mode 100644 index 0000000..7edf553 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/common/not-found-view.html @@ -0,0 +1,5 @@ +

+ +
+

+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/search/dirPagination.tpl.html b/querydsl/frontend/app/assets/partials/search/dirPagination.tpl.html new file mode 100644 index 0000000..558aa20 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/search/dirPagination.tpl.html @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/search/search-form-directive.html b/querydsl/frontend/app/assets/partials/search/search-form-directive.html new file mode 100644 index 0000000..674143e --- /dev/null +++ b/querydsl/frontend/app/assets/partials/search/search-form-directive.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/search/search-result-list-directive.html b/querydsl/frontend/app/assets/partials/search/search-result-list-directive.html new file mode 100644 index 0000000..c38f4f7 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/search/search-result-list-directive.html @@ -0,0 +1,19 @@ +
+ + +
+ +
+
+
+

+
diff --git a/querydsl/frontend/app/assets/partials/search/search-result-view.html b/querydsl/frontend/app/assets/partials/search/search-result-view.html new file mode 100644 index 0000000..2d8cd39 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/search/search-result-view.html @@ -0,0 +1,4 @@ +
+

+
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/add-todo-view.html b/querydsl/frontend/app/assets/partials/todo/add-todo-view.html new file mode 100644 index 0000000..0a0406a --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/add-todo-view.html @@ -0,0 +1,9 @@ +

+ +
+
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/delete-todo-modal.html b/querydsl/frontend/app/assets/partials/todo/delete-todo-modal.html new file mode 100644 index 0000000..b390319 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/delete-todo-modal.html @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/edit-todo-view.html b/querydsl/frontend/app/assets/partials/todo/edit-todo-view.html new file mode 100644 index 0000000..1695ae6 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/edit-todo-view.html @@ -0,0 +1,8 @@ +

+
+
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/todo-form-directive.html b/querydsl/frontend/app/assets/partials/todo/todo-form-directive.html new file mode 100644 index 0000000..c7815d0 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/todo-form-directive.html @@ -0,0 +1,52 @@ +
+
+ : + +
+ + +
+
+
+ : + +
+ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/todo-list-directive.html b/querydsl/frontend/app/assets/partials/todo/todo-list-directive.html new file mode 100644 index 0000000..60ed955 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/todo-list-directive.html @@ -0,0 +1,8 @@ +
+

+
+ diff --git a/querydsl/frontend/app/assets/partials/todo/todo-list-view.html b/querydsl/frontend/app/assets/partials/todo/todo-list-view.html new file mode 100644 index 0000000..6a83ba4 --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/todo-list-view.html @@ -0,0 +1,7 @@ +
+

+ +
+ +
+
\ No newline at end of file diff --git a/querydsl/frontend/app/assets/partials/todo/view-todo-view.html b/querydsl/frontend/app/assets/partials/todo/view-todo-view.html new file mode 100644 index 0000000..374c16d --- /dev/null +++ b/querydsl/frontend/app/assets/partials/todo/view-todo-view.html @@ -0,0 +1,25 @@ +
+

+ +
+

{{todoEntry.title}}

+

{{todoEntry.description}}

+
+

+ + {{"todo.creation.time" | translate}}: {{todoEntry.creationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.created.by.prefix" | translate}} {{todoEntry.createdByUser}} + {{"todo.modification.time" | translate }}: {{todoEntry.modificationTime | amDateFormat:'DD.MM.YYYY HH:mm:ss'}} + {{"todo.modified.by.prefix" | translate}} {{todoEntry.modifiedByUser}} + +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/querydsl/frontend/app/module/account/account.config.js b/querydsl/frontend/app/module/account/account.config.js new file mode 100644 index 0000000..c689bb1 --- /dev/null +++ b/querydsl/frontend/app/module/account/account.config.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('app.account.config', []) + .constant('AUTH_EVENTS', { + loginSuccess: 'event:auth-login-success', + loginFailed: 'event:auth-login-failed', + logoutSuccess: 'event:auth-logout-success', + sessionTimeout: 'event:auth-session-timeout', + notAuthenticated: 'event:auth-loginRequired', + notAuthorized: 'event:auth-forbidden' + }) + .config(['csrfProvider', function(csrfProvider) { + // optional configurations + csrfProvider.config({ + httpTypes: ['PUT', 'POST', 'DELETE'], + maxRetries: 1, + url: '/api/csrf' + }); + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/account/account.controllers.js b/querydsl/frontend/app/module/account/account.controllers.js new file mode 100644 index 0000000..79f6fbe --- /dev/null +++ b/querydsl/frontend/app/module/account/account.controllers.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('app.account.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.login', { + url: 'login', + controller: 'LoginController', + templateUrl: 'account/login-view.html' + }) + .state('todo.forbidden', { + url: 'forbidden', + controller: 'ForbiddenController', + templateUrl: 'account/forbidden-view.html' + }); + } + ]) + .controller('ForbiddenController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.ForbiddenController'); + logger.info("Rendering forbidden view."); + }]) + .controller('LoginController', ['$log', function($log) { + var logger = $log.getInstance('app.account.controllers.LoginController'); + logger.info('Rendering login form.'); + }]); + diff --git a/querydsl/frontend/app/module/account/account.directives.js b/querydsl/frontend/app/module/account/account.directives.js new file mode 100644 index 0000000..2ff0aa6 --- /dev/null +++ b/querydsl/frontend/app/module/account/account.directives.js @@ -0,0 +1,44 @@ +'use strict'; + +angular.module('app.account.directives', []) + .directive('logOutLink', ['$log', 'AuthenticationService', function ($log, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.logOutLink'); + + return { + link: function (scope, element, attr) { + scope.logOut = function() { + logger.info('Logging user out.'); + AuthenticationService.logOut(); + }; + }, + templateUrl: 'account/logout-link-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('loginForm', ['$log', 'AUTH_EVENTS', 'AuthenticationService', function ($log, AUTH_EVENTS, AuthenticationService) { + + var logger = $log.getInstance('app.account.directives.loginForm'); + + return { + link: function (scope, element, attr) { + scope.login = {}; + scope.loginFailed = false; + + scope.$on(AUTH_EVENTS.loginFailed, function() { + logger.info('Received login failed event.'); + scope.loginFailed = true; + }); + + scope.submitLoginForm = function() { + logger.info('Submitting log in form.'); + AuthenticationService.logIn(scope.login.username, scope.login.password); + }; + }, + templateUrl: 'account/login-form-directive.html', + scope: { + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/account/account.services.js b/querydsl/frontend/app/module/account/account.services.js new file mode 100644 index 0000000..7cbd82d --- /dev/null +++ b/querydsl/frontend/app/module/account/account.services.js @@ -0,0 +1,79 @@ +'use strict'; + +angular.module('app.account.services', ['ngResource']) + .service('AuthenticatedUser', function () { + this.create = function (username, role) { + this.username = username; + this.role = role; + }; + this.destroy = function () { + this.username = null; + this.role = null; + }; + }) + .factory('AuthenticationService', ['$http', '$log', '$rootScope', '$state', 'AUTH_EVENTS', 'AuthenticatedUser', + function($http, $log, $rootScope, $state, AUTH_EVENTS, AuthenticatedUser) { + + var logger = $log.getInstance('app.account.services.AuthenticationService'); + + return { + authorizeStateChange: function(event, toState, toParams) { + logger.debug('Authorizing state change to state: %s', toState.name); + if (toState.authenticate && !this.isAuthenticated()) { + event.preventDefault(); + + logger.debug('Authentication is not found. Fetching it from the backend.'); + var self = this; + $http.get('/api/authenticated-user').success(function(user) { + logger.debug('Found authenticated user: %j', user); + AuthenticatedUser.create(user.username, user.role); + + if (!self.isAuthenticated) { + logger.debug('Unauthenticated users is: %j', AuthenticatedUser); + $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); + } + else { + logger.debug('User is authenticated. Continuing to the target state: %s', toState.name); + $state.go(toState.name, toParams); + } + }); + } + }, + isAuthenticated: function() { + logger.debug('Checking if user: %j is authenticated.', AuthenticatedUser); + return AuthenticatedUser.username; + }, + logIn: function(username, password) { + logger.info('Logging in user with username: %s', username); + + var transform = function(data){ + return $.param(data); + }; + + $http.post('/api/login', {username: username, password: password}, { + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + ignoreAuthModule: true, + transformRequest: transform + }) + .success(function(user) { + logger.info('Login successful for user: %j', user); + AuthenticatedUser.create(user.username, user.role); + $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); + }) + .error(function() { + logger.info('Login failed'); + $rootScope.$broadcast(AUTH_EVENTS.loginFailed); + }); + }, + logOut: function() { + if (this.isAuthenticated()) { + $http.post('/api/logout', {}) + .success(function() { + logger.info('User is logged out.'); + AuthenticatedUser.destroy(); + $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); + }); + } + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/common/common.config.js b/querydsl/frontend/app/module/common/common.config.js new file mode 100644 index 0000000..6ffca02 --- /dev/null +++ b/querydsl/frontend/app/module/common/common.config.js @@ -0,0 +1,60 @@ +'use strict'; + +angular.module('app.common.config', []) + .constant('COMMON_EVENTS', { + notFound: 'event:not-found' + }) + .config(['logEnhancerProvider', function (logEnhancerProvider) { + logEnhancerProvider.datetimePattern = 'DD.MM.YYYY HH:mm:ss'; + logEnhancerProvider.prefixPattern = '%s::[%s]> '; + logEnhancerProvider.logLevels = { + '*': logEnhancerProvider.LEVEL.OFF + }; + }]) + .config(['$urlRouterProvider', '$locationProvider', + function ($urlRouterProvider, $locationProvider) { + //this prevents infinite $digest loop when we invoke the + //preventDefault() method in $stateChangeStart event handler. + //See: https://github.com/angular-ui/ui-router/issues/600#issuecomment-47228922 + $urlRouterProvider.otherwise( function($injector, $location) { + var $state = $injector.get("$state"); + $state.go("todo.list"); + }); + + // Without server side support html5 must be disabled. + $locationProvider.html5Mode(false); + } + ]) + .config(['$translateProvider', function ($translateProvider) { + // Initialize angular-translate + $translateProvider.useStaticFilesLoader({ + prefix: '/i18n/', + suffix: '.json' + }); + + $translateProvider.preferredLanguage('en'); + $translateProvider.useSanitizeValueStrategy('escaped'); + $translateProvider.useLocalStorage(); + $translateProvider.useMissingTranslationHandlerLog(); + }]) + .config(['growlProvider', function (growlProvider) { + growlProvider.globalTimeToLive(5000); + }]) + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push([ + '$injector', + function ($injector) { + return $injector.get('404Interceptor'); + } + ]); + }]) + .factory('404Interceptor', ['$rootScope', '$q', 'COMMON_EVENTS', function ($rootScope, $q, COMMON_EVENTS) { + return { + responseError: function(response) { + if (response.status === 404) { + $rootScope.$broadcast(COMMON_EVENTS.notFound); + } + return $q.reject(response); + } + }; + }]); diff --git a/querydsl/frontend/app/module/common/common.controllers.js b/querydsl/frontend/app/module/common/common.controllers.js new file mode 100644 index 0000000..811f15e --- /dev/null +++ b/querydsl/frontend/app/module/common/common.controllers.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('app.common.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.404', { + url: 'not-found', + controller: 'NotFoundController', + templateUrl: 'common/not-found-view.html' + }); + } + ]) + .controller('NotFoundController', ['$log', function($log) { + var logger = $log.getInstance('app.common.controllers.NotFoundController'); + logger.info("Rendering 404 view."); + }]); + diff --git a/querydsl/frontend/app/module/common/common.directives.js b/querydsl/frontend/app/module/common/common.directives.js new file mode 100644 index 0000000..7c56027 --- /dev/null +++ b/querydsl/frontend/app/module/common/common.directives.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('app.common.directives', []) + .directive('staticInclude', ['$http', '$templateCache', '$compile', function ($http, $templateCache, $compile) { + return function(scope, element, attrs) { + var templatePath = attrs.staticInclude; + + $http.get(templatePath, {cache: $templateCache}).success(function (response) { + var contents = $('
').html(response).contents(); + element.html(contents); + $compile(contents)(scope); + }); + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/common/common.services.js b/querydsl/frontend/app/module/common/common.services.js new file mode 100644 index 0000000..de9d0e6 --- /dev/null +++ b/querydsl/frontend/app/module/common/common.services.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('app.common.services', []) + .service('NotificationService', ['$rootScope', 'growl', function ($rootScope, growl) { + var flashMessageQueue = []; + + function displayNotification(message, type) { + if (type === 'success') { + growl.success(message); + } else if (type === 'warn') { + growl.warning(message); + } else if (type === 'info') { + growl.info(message); + } else { + growl.error(message); + } + } + + // Display all flash notifications after state has changed + $rootScope.$on("$stateChangeSuccess", function () { + while (flashMessageQueue.length > 0) { + var item = flashMessageQueue.shift(); + if (item) { + displayNotification(item.message, item.type); + } + } + }); + + // Public API + return { + 'flashMessage': function (message, type) { + flashMessageQueue.push({message: message, type: type || 'info'}); + } + }; + }]); diff --git a/querydsl/frontend/app/module/search/search.controllers.js b/querydsl/frontend/app/module/search/search.controllers.js new file mode 100644 index 0000000..6fb86a5 --- /dev/null +++ b/querydsl/frontend/app/module/search/search.controllers.js @@ -0,0 +1,41 @@ +'use strict'; + +angular.module('app.search.controllers', []) + .constant('paginationConfig', { + firstPageNumber: 1, + pageSize: 5 + }) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo.search', { + authenticate: true, + url: 'todo/search/:searchTerm/page/:pageNumber/size/:pageSize', + controller: 'SearchResultController', + templateUrl: 'search/search-result-view.html', + resolve: { + searchResults: ['TodoSearchService', '$stateParams', function(TodoSearchService, $stateParams) { + if ($stateParams.searchTerm) { + return TodoSearchService.findBySearchTerm($stateParams.searchTerm, + $stateParams.pageNumber - 1, + $stateParams.pageSize + ); + } + + return null; + }], + searchTerm: ['$stateParams', function($stateParams) { + return $stateParams.searchTerm; + }] + } + }); + } + ]) + .controller('SearchResultController', ['$log', '$scope', '$state', 'paginationConfig', 'searchResults', 'searchTerm', + function($log, $scope, $state, paginationConfig, searchResults, searchTerm) { + var logger = $log.getInstance('app.search.controllers.SearchResultController'); + logger.info('Rendering search results page for search term: %s with search results: %j', searchTerm, searchResults); + $scope.searchResults = searchResults; + $scope.searchTerm = searchTerm; + }]); + diff --git a/querydsl/frontend/app/module/search/search.directives.js b/querydsl/frontend/app/module/search/search.directives.js new file mode 100644 index 0000000..ecefb9c --- /dev/null +++ b/querydsl/frontend/app/module/search/search.directives.js @@ -0,0 +1,104 @@ +'use strict'; + +angular.module('app.search.directives', []) + .directive('searchForm', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + + var logger = $log.getInstance('app.search.directives.searchForm'); + + return { + link: function (scope, element, attr) { + var userWritingSearchTerm = false; + var minimumSearchTermLength = 3; + + scope.translationData = { + missingCharCount: minimumSearchTermLength + }; + + scope.search = {}; + scope.search.searchTerm = ""; + + scope.searchFieldBlur = function() { + userWritingSearchTerm = false; + scope.search.searchTerm = ""; + scope.translationData.missingCharCount = minimumSearchTermLength; + }; + + scope.searchFieldFocus = function() { + userWritingSearchTerm = true; + }; + + scope.showMissingCharacterText = function() { + if (!scope.search.searchTerm) { + scope.search.searchTerm = ""; + } + + if (userWritingSearchTerm) { + if (scope.search.searchTerm.length < minimumSearchTermLength) { + return true; + } + } + + return false; + }; + + scope.search = function() { + logger.trace('User is using the search term: %s', scope.search.searchTerm); + + if (scope.search.searchTerm.length < minimumSearchTermLength) { + scope.translationData.missingCharCount = minimumSearchTermLength - scope.search.searchTerm.length; + logger.trace('%s characters are missing. Search is not invoked.', scope.translationData.missingCharCount); + } + else { + scope.translationData.missingCharCount = 0; + $state.go('todo.search', + { + searchTerm: scope.search.searchTerm, + pageNumber: paginationConfig.firstPageNumber, + pageSize: paginationConfig.pageSize + }, + {reload: true, inherit: true, notify: true} + ); + } + }; + + }, + templateUrl: 'search/search-form-directive.html', + scope: { + currentUser: '=' + } + }; + }]) + .directive('searchResultList', ['$log', '$state', 'paginationConfig', function($log, $state, paginationConfig) { + var logger = $log.getInstance('app.search.directives.searchResultList'); + + return { + link: function(scope, element, attr) { + logger.debug("Rendering search result list for search term: %s and search results: %j", scope.searchTerm, scope.searchResults); + scope.todoEntries = scope.searchResults.content; + + scope.pagination = { + currentPage: scope.searchResults.number + 1, + itemsPerPage: paginationConfig.pageSize, + totalItems: scope.searchResults.totalElements + }; + + scope.pageChanged = function(newPageNumber) { + logger.debug('Requesting a new page: %s for search term: %s with page size: %s', + newPageNumber, + scope.searchTerm, + paginationConfig.pageSize + ); + + $state.go('todo.search', + {searchTerm: scope.searchTerm, pageNumber: newPageNumber, pageSize: paginationConfig.pageSize}, + {reload: true, inherit: true, notify: true} + ); + }; + }, + templateUrl: 'search/search-result-list-directive.html', + scope: { + searchResults: '=', + searchTerm: '@' + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/search/search.services.js b/querydsl/frontend/app/module/search/search.services.js new file mode 100644 index 0000000..e007227 --- /dev/null +++ b/querydsl/frontend/app/module/search/search.services.js @@ -0,0 +1,22 @@ +'use strict'; + +angular.module('app.search.services', ['ngResource']) + .factory('TodoSearchService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/search', {}, { + 'query': {method:'GET', isArray:false} + }); + + var logger = $log.getInstance('app.search.services.TodoSearchService'); + + return { + findBySearchTerm: function(searchTerm, pageNumber, pageSize) { + logger.info('Searching todo entries with search term: %s, pageNumber: %s, and page size: %s', searchTerm, pageNumber, pageSize); + return api.query({ + page: pageNumber, + searchTerm: searchTerm, + size: pageSize, + sort: "title" + }).$promise; + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/todo/todo.controllers.js b/querydsl/frontend/app/module/todo/todo.controllers.js new file mode 100644 index 0000000..d4da7bf --- /dev/null +++ b/querydsl/frontend/app/module/todo/todo.controllers.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('app.todo.controllers', []) + .config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('todo', { + url: '/', + abstract: true, + template: '' + }) + .state('todo.add', { + authenticate: true, + url: 'todo/add', + controller: 'AddTodoController', + templateUrl: 'todo/add-todo-view.html' + }) + .state('todo.edit', { + authenticate: true, + url: 'todo/:id/edit', + controller: 'EditTodoController', + templateUrl: 'todo/edit-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }) + .state('todo.list', { + authenticate: true, + url: '', + controller: 'TodoListController', + templateUrl: 'todo/todo-list-view.html', + resolve: { + todoEntries: ['TodoService', function(TodoService) { + return TodoService.findAll(); + }] + } + }) + .state('todo.view', { + authenticate: true, + url: 'todo/:id', + controller: 'ViewTodoController', + templateUrl: 'todo/view-todo-view.html', + resolve: { + todoEntry: ['$stateParams', 'TodoService', function($stateParams, TodoService) { + return TodoService.findById($stateParams.id); + }] + } + }); + } + ]) + .controller('AddTodoController', ['$log', '$scope', function($log, $scope) { + var logger = $log.getInstance('app.todo.controllers.AddTodoController'); + logger.info('Rendering add todo entry page.'); + $scope.todoEntry = {}; + }]) + .controller('EditTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.EditTodoController'); + logger.info('Rendering edit todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]) + .controller('TodoListController', ['$log', '$scope', 'todoEntries', function($log, $scope, todoEntries) { + var logger = $log.getInstance('app.todo.controllers.TodoListController'); + logger.info('Rendering todo entry list page for %s todo entries.', todoEntries.length); + $scope.todoEntries = todoEntries; + }]) + .controller('ViewTodoController', ['$log', '$scope', 'todoEntry', function($log, $scope, todoEntry) { + var logger = $log.getInstance('app.todo.controllers.ViewTodoController'); + logger.info('Rendering view todo entry page for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/todo/todo.directives.js b/querydsl/frontend/app/module/todo/todo.directives.js new file mode 100644 index 0000000..a2377b5 --- /dev/null +++ b/querydsl/frontend/app/module/todo/todo.directives.js @@ -0,0 +1,102 @@ +'use strict'; + +angular.module('app.todo.directives', []) + .controller('DeleteTodoController', ['$log', '$scope', '$modalInstance', '$state', 'TodoService', 'todoEntry', 'successCallback', 'errorCallback', + function($log, $scope, $modalInstance, $state, TodoService, todoEntry, successCallback, errorCallback) { + var logger = $log.getInstance('app.todo.directives.DeleteTodoController'); + + logger.info('Showing delete confirmation dialog for todo entry: %j', todoEntry); + $scope.todoEntry = todoEntry; + + $scope.cancel = function() { + logger.info('User clicked cancel button. Todo entry is not deleted.'); + $modalInstance.dismiss('cancel'); + }; + + $scope.delete = function() { + logger.info('User clicked delete button. Todo entry is deleted.'); + $modalInstance.close(); + TodoService.delete(todoEntry, successCallback, errorCallback); + }; + }]) + .directive('deleteTodoEntryButton', ['$modal', '$state', 'NotificationService', function($modal, $state, NotificationService) { + return { + link: function (scope, element, attr) { + scope.onSuccess = function() { + NotificationService.flashMessage('todo.notifications.delete.success', 'success'); + $state.go('todo.list'); + }; + + scope.onError = function() { + NotificationService.flashMessage('todo.notifications.delete.error', 'error'); + }; + + scope.showDeleteConfirmationDialog = function() { + $modal.open({ + templateUrl: 'todo/delete-todo-modal.html', + controller: 'DeleteTodoController', + resolve: { + errorCallback: function() { + return scope.onError; + }, + successCallback: function() { + return scope.onSuccess; + }, + todoEntry: function () { + return scope.todoEntry; + } + } + }); + }; + }, + template: '', + scope: { + todoEntry: '=' + } + }; + }]) + .directive('todoEntryForm', ['$log', '$state', 'NotificationService', 'TodoService', function($log, $state, NotificationService, TodoService) { + var logger = $log.getInstance('app.todo.directives.todoEntryForm'); + + return { + link: function (scope, element, attr) { + scope.saveTodoEntry = function() { + logger.info('Saving todo entry: %j', scope.todoEntry); + + var onSuccess = function(saved) { + NotificationService.flashMessage(scope.successMessageKey, 'success'); + $state.go('todo.view', {id: saved.id}); + }; + + var onError = function() { + NotificationService.flashMessage(scope.errorMessageKey, 'errors'); + }; + + if (scope.formType === 'add') { + TodoService.add(scope.todoEntry, onSuccess, onError); + } + else if (scope.formType === 'edit') { + TodoService.update(scope.todoEntry, onSuccess, onError); + } + else { + logger.error('Unknown form type: %s', scope.formType); + } + }; + }, + templateUrl: 'todo/todo-form-directive.html', + scope: { + errorMessageKey: '@', + formType: '@', + todoEntry: '=', + successMessageKey: '@' + } + }; + }]) + .directive('todoEntryList', [function() { + return { + templateUrl: 'todo/todo-list-directive.html', + scope: { + todoEntries: '=' + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/module/todo/todo.services.js b/querydsl/frontend/app/module/todo/todo.services.js new file mode 100644 index 0000000..f49f622 --- /dev/null +++ b/querydsl/frontend/app/module/todo/todo.services.js @@ -0,0 +1,61 @@ +'use strict'; + +angular.module('app.todo.services', ['ngResource']) + .factory('TodoService', ['$log', '$resource', function($log, $resource) { + var api = $resource('/api/todo/:id', {"id": "@id"}, { + get: {method: 'GET'}, + save: {method: 'POST'}, + update: {method: 'PUT'}, + query: {method: 'GET', params: {}, isArray: true} + }); + + var logger = $log.getInstance('app.todo.services.TodoService'); + + return { + add: function(todo, successCallback, errorCallback) { + logger.info('Adding new todo entry: %j', todo); + return api.save(todo, + function(added) { + logger.info('Added a new todo entry: %j', added); + successCallback(added); + }, + function(error) { + logger.error('Adding a todo entry failed because of an error: %j', error); + errorCallback(error); + }); + }, + delete: function(todo, successCallback, errorCallback) { + logger.info('Deleting todo entry: %j', todo); + return api.delete(todo, + function(deleted) { + logger.info('Deleted todo entry: %j', deleted); + successCallback(deleted); + }, + function(error) { + logger.error('Deleting the todo entry failed because of an error: %j', error); + errorCallback(error); + } + ); + }, + findAll: function() { + logger.info('Finding all todo entries.'); + return api.query(); + }, + findById: function(id) { + logger.info('Finding todo entry by id: %s', id); + return api.get({id: id}).$promise; + }, + update: function(todo, successCallback, errorCallback) { + logger.info('Updating todo entry: %j', todo); + return api.update(todo, + function(updated) { + logger.info('Updated the information of the todo entry: %j', updated); + successCallback(updated); + }, + function(error) { + logger.error('Updating the information of the todo entry failed because of an error: %j', error); + errorCallback(error); + }); + } + }; + }]); \ No newline at end of file diff --git a/querydsl/frontend/app/styles/app.less b/querydsl/frontend/app/styles/app.less new file mode 100644 index 0000000..4e70998 --- /dev/null +++ b/querydsl/frontend/app/styles/app.less @@ -0,0 +1,74 @@ +[ng-cloak] { + display: none; +} + +@import "https://github.com/bower_components/bootstrap/less/bootstrap.less"; + +// Red asterisk for required labels +label.required:before{ + content:"* "; + color:red; +} + +// styles for custom input validation +input.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +input.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +input.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-pristine { + border: 1px solid #cccccc; +} + +textarea.form-control.ng-pristine.ng-invalid.ng-submitted { + border: 1px solid #f00; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid.ng-focused { + border: 1px solid #cccccc; + background-color: #ffffff; +} + +textarea.form-control.ng-dirty.ng-invalid { + border: 1px solid #f00; + background-color: #ffffff; +} + +small.ng-error { + color: #a94442; +} + +a:hover { + cursor: pointer; +} + +.striped-list { + > .row:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + +.striped-list .row { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; +} + +.action-buttons { + text-align: right; +} diff --git a/querydsl/frontend/bower.json b/querydsl/frontend/bower.json new file mode 100644 index 0000000..8390db6 --- /dev/null +++ b/querydsl/frontend/bower.json @@ -0,0 +1,40 @@ +{ + "name": "Spring Data JPA Tutorial - Query Methods", + "version": "0.0.1", + "main": "_public/frontend/js/app.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "console-polyfill": "~0.2.1", + "lodash": "~3.8.0", + "moment": "2.10.6", + "jquery": "2.1.0", + "bootstrap": "~3.3.4", + "angular": "~1.3.15", + "angular-http-auth": "1.2.2", + "angular-i18n": "~1.3.15", + "angular-moment": "0.10.1", + "angular-logger": "1.0.1", + "angular-sanitize": "~1.3.15", + "angular-resource": "~1.3.15", + "angular-cookies": "~1.3.15", + "angular-loader": "~1.3.15", + "angular-mocks": "~1.3.15", + "angular-translate": "~2.7.0", + "angular-translate-storage-local": "~2.7.0", + "angular-translate-loader-static-files": "~2.7.0", + "angular-translate-handler-log": "~2.7.0", + "angular-ui-utils": "~0.2.3", + "angular-ui-router": "~0.2.15", + "angular-bootstrap": "~0.13.0", + "angular-growl-v2": "0.7.3", + "angular-utils-pagination": "0.8.2", + "es5-shim": "~4.1.1", + "json3": "~3.3.2", + "script.js": "~2.5.7", + "sprintf": "1.0.3" + } +} \ No newline at end of file diff --git a/querydsl/frontend/build.config.js b/querydsl/frontend/build.config.js new file mode 100644 index 0000000..963b2f3 --- /dev/null +++ b/querydsl/frontend/build.config.js @@ -0,0 +1,77 @@ +'use strict'; + +var path = require('path'); + +var targetBase = './build/'; + +module.exports = { + //Configures the directories in which the files created by Gulp are copied. + target: { + js: targetBase + '/js', + lib: path.join(targetBase, 'js', 'lib'), + css: path.join(targetBase, 'css'), + partials: path.join(targetBase, 'partials'), + assets: targetBase + }, + + //Configures the location of the used libraries and frameworks. + vendorFiles: { + code: [ + './bower_components/console-polyfill/index.js', + './bower_components/lodash/dist/lodash.min.js', + './bower_components/jquery/dist/jquery.min.js', + './bower_components/angular/angular.js', + './bower_components/moment/min/moment-with-locales.min.js', + './bower_components/sprintf/dist/sprintf.min.js', + './bower_components/angular-http-auth/src/http-auth-interceptor.js', + './bower_components/angular-i18n/angular-locale_fi-fi.js', + './bower_components/angular-cookies/angular-cookies.min.js', + './bower_components/angular-moment/angular-moment.min.js', + './bower_components/angular-logger/dist/angular-logger.min.js', + './bower_components/angular-resource/angular-resource.min.js', + './bower_components/angular-sanitize/angular-sanitize.min.js', + './bower_components/angular-translate/angular-translate.min.js', + './bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', + './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js', + './bower_components/angular-translate-storage-local/angular-translate-storage-local.min.js', + './bower_components/angular-translate-handler-log/angular-translate-handler-log.min.js', + './bower_components/angular-ui-router/release/angular-ui-router.min.js', + './bower_components/angular-ui-utils/ui-utils.min.js', + './bower_components/angular-ui-utils/ui-utils-ieshiv.min.js', + './bower_components/angular-utils-pagination/dirPagination.js', + './bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', + './bower_components/angular-growl-v2/build/angular-growl.min.js', + './vendor/spring-security-csrf-token-interceptor/src/spring-security-csrf-token-interceptor.js' + ] + }, + + //Configures the location of our application's files. + appFiles: { + //Configures the location of the Javascript files. + code: [ + "./app/**/*.js" + ], + //Configures the location of the LESS files. + styleBase: "./app/styles/", + style: [ + "./bower_components/angular-growl-v2/build/angular-growl.min.css", + "./app/styles/app.less" + ], + //Configures the location of the view templates. + partials: [ + "./app/assets/partials/**/*.html" + ], + //Configures the location of static assets such as images, fonts, and localization files. + assetsBase: './app/assets/', + assets: [ + './app/assets/**' + ], + //Configures the location of shims (libraries that bring new APIs to older browsers) + shim: [ + './bower_components/angular-loader/angular-loader.min.js', + './bower_components/script.js/dist/script.min.js', + './bower_components/es5-shim/es5-shim.min.js', + './bower_components/json3/lib/json3.min.js' + ] + } +}; \ No newline at end of file diff --git a/querydsl/frontend/gulpfile.js b/querydsl/frontend/gulpfile.js new file mode 100644 index 0000000..40f10f7 --- /dev/null +++ b/querydsl/frontend/gulpfile.js @@ -0,0 +1,124 @@ +var gulp = require("gulp"); +var plugins = require('gulp-load-plugins')(); +var config = require('./build.config.js'); + +//Analyzes the Javascript files of our application by using JSHint and reports the found problems. +gulp.task('jshint', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.changed(config.target.js)) + .pipe(plugins.jshint('.jshintrc')) + .pipe(plugins.jshint.reporter('jshint-stylish')); +}); + +//Processes the Javascript files of our application. +gulp.task('appCode', function () { + return gulp.src(config.appFiles.code) + .pipe(plugins.sourcemaps.init()) + //Combines the Javascript files into a single Javascript file + .pipe(plugins.concat('app.min.js')) + //Minifies the created Javascript file + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.sourcemaps.write()) + //Copies the minified Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file. + .pipe(plugins.size({title: 'application'})) +}); + +//Processes the HTML templates of our application. +gulp.task('appPartials', function () { + return gulp.src(config.appFiles.partials) + .pipe(plugins.changed(config.target.js)) + //Minifies the HTML files + .pipe(plugins.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + //Loads the HTML templates into AngularJS $templateCache + .pipe(plugins.angularTemplatecache('partials.js', { + standalone: true + })) + //Copy the created Javascript file to the target directory + .pipe(gulp.dest(config.target.js)) + //Reports the size of created Javascript file + .pipe(plugins.size({showFiles: true})) +}); + +//Processes the LESS files of our application. +gulp.task('appLess', function () { + return gulp.src(config.appFiles.style) + //Creates the final CSS file + .pipe(plugins.less({ + paths: [config.appFiles.styleBase] + })) + .pipe(plugins.concat('app.css')) + //Minifies the created CSS file + .pipe(plugins.minifyCss()) + //Copies the CSS File into the target directory + .pipe(gulp.dest(config.target.css)) + //Reports the size of the final CSS file. + .pipe(plugins.size({ title: 'css' })) +}); + +gulp.task('appAssets', function () { + return gulp.src(config.appFiles.assets, {base: config.appFiles.assetsBase}) + .pipe(gulp.dest(config.target.assets)) +}); + +//Minimizes the shims used by our application and copies them to the target directory. +gulp.task('appShim', function () { + return gulp.src(config.appFiles.shim) + .pipe(plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + })) + .pipe(gulp.dest(config.target.lib)); +}); + +//Processes the Javascript files of the libraries and frameworks that are used in our application +gulp.task('vendorCode', function () { + return gulp.src(config.vendorFiles.code) + //Combine the Javascript files into a single Javascript file + .pipe(plugins.concat('vendor.min.js')) + //Skips minification of files that are already minified. + .pipe(plugins.if('*.min.js', plugins.uglify({ + mangle: false, + compress: false, + preserveComments: 'some' + }))) + //Minifies Javascript files that are not minified. + .pipe(plugins.if('vendor/**/*.js', plugins.uglify({ + mangle: false, + compress: true + }))) + //Copies the created file to the target directory. + .pipe(gulp.dest(config.target.js)) + //Reports the size of the final Javascript file + .pipe(plugins.size({title: 'vendor'})) +}); + +//Analyzes our Javascript files by using JSHint and invokes the build when the watched files are changed +gulp.task('watch', ['jshint', 'build'], function () { + gulp.watch(config.appFiles.partials, ['appPartials']); + gulp.watch(config.appFiles.code, ['appCode', 'jshint']); + gulp.watch(config.appFiles.style, ['appLess']); + gulp.watch(config.appFiles.assets, ['appAssets']); + gulp.watch(config.vendorFiles.code, ['vendorCode']); +}); + +//Configures the tasks of our build +gulp.task('build', [ + 'appLess', + 'appShim', + 'appAssets', + 'appPartials', + 'appCode', + 'vendorCode' +]); + +//Runs the watch task if no task is specified when gulp is run +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/querydsl/frontend/package.json b/querydsl/frontend/package.json new file mode 100644 index 0000000..55dfccd --- /dev/null +++ b/querydsl/frontend/package.json @@ -0,0 +1,38 @@ +{ + "author": "Petri Kainulainen", + "name": "spring-data-jpa-tutorial-query-methods", + "description": "Angular frontend for a Spring Data JPA example.", + "version": "1.0.0", + "homepage": "", + "repository": { + "type": "git", + "url": "" + }, + "dependencies": { + "bower": "~1.4.1", + "gulp": "~3.8.11", + "gulp-angular-templatecache": "~1.6.0", + "gulp-changed": "~1.2.1", + "gulp-concat": "~2.5.2", + "gulp-if": "~1.2.5", + "gulp-insert": "^0.4.0", + "gulp-jshint": "~1.10.0", + "gulp-less": "~3.0.3", + "gulp-load-plugins": "~0.10.0", + "gulp-minify-css": "~1.1.1", + "gulp-minify-html": "~1.0.2", + "gulp-rename": "~1.2.2", + "gulp-size": "~1.2.1", + "gulp-sourcemaps": "~1.5.2", + "gulp-uglify": "~1.2.0", + "jshint-stylish": "~1.0.2" + }, + "engines": { + "node": ">=0.12.0" + } +} + + + + + diff --git a/querydsl/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js b/querydsl/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js new file mode 100644 index 0000000..84318da --- /dev/null +++ b/querydsl/frontend/vendor/spring-security-csrf-token-interceptor/dist/spring-security-csrf-token-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("spring-security-csrf-token-interceptor",[]).factory("csrfInterceptor",["$injector","$q",function($injector){var $q=$injector.get("$q"),csrf=$injector.get("csrf"),csrfService=csrf.init();return{request:function(config){return csrfService.settings.httpTypes.indexOf(config.method.toUpperCase())>-1&&(config.headers[csrfService.settings.csrfTokenHeader]=csrfService.token),config||$q.when(config)},responseError:function(response){var $http,newToken=response.headers(csrfService.settings.csrfTokenHeader);return 403===response.status&&csrfService.numRetries -1) { + config.headers[csrfService.settings.csrfTokenHeader] = csrfService.token; + } + return config || $q.when(config); + }, + responseError: function(response) { + var $http, + newToken = response.headers(csrfService.settings.csrfTokenHeader); + + if (response.status === 403 && csrfService.numRetries < csrfService.settings.maxRetries) { + csrfService.getTokenData(); + $http = $injector.get('$http'); + csrfService.numRetries = csrfService.numRetries + 1; + return $http(response.config); + } else if (newToken) { + // update the csrf token in-case of response errors other than 403 + csrfService.token = newToken; + } + // Fix for interceptor causing failing requests + return $q.reject(response); + }, + response: function(response) { + // reset number of retries on a successful response + csrfService.numRetries = 0; + return response; + } + }; + } + ]).factory('csrfService', [ + + function() { + var defaults = { + url: '/', // the URL to which the CSRF call has to be made to get the token + csrfHttpType: 'head', // the HTTP method type which is used for making the CSRF token call + maxRetries: 5, // number of retires allowed for forbidden requests + csrfTokenHeader: 'X-CSRF-TOKEN', + httpTypes: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] // default allowed HTTP types + }; + return { + inited: false, + settings: null, + numRetries: 0, + token: '', + init: function(options) { + this.settings = angular.extend({}, defaults, options); + this.getTokenData(); + console.log(this.settings, this.defaults, options); + }, + getTokenData: function() { + var xhr = new XMLHttpRequest(); + xhr.open(this.settings.csrfHttpType, this.settings.url, false); + xhr.send(); + + this.token = xhr.getResponseHeader(this.settings.csrfTokenHeader); + this.inited = true; + } + }; + + } + ]).provider('csrf', [ + + function() { + var CsrfModel = function CsrfModel(options) { + return { + options: options, + csrfService: null + }; + }; + + return { + $get: ['csrfService', + function(csrfService) { + var self = this; + return { + init: function() { + self.model = new CsrfModel(self.options); + self.model.csrfService = csrfService; + self.model.csrfService.init(self.model.options); + return self.model.csrfService; + } + }; + } + ], + + model: null, + + options: {}, + + config: function(options) { + this.options = options; + } + }; + } + ]).config(['$httpProvider', + function($httpProvider) { + $httpProvider.interceptors.push('csrfInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/querydsl/pom.xml b/querydsl/pom.xml new file mode 100644 index 0000000..9e1ed40 --- /dev/null +++ b/querydsl/pom.xml @@ -0,0 +1,404 @@ + + 4.0.0 + net.petrikainulainen.springdata.jpa + querydsl + 0.1 + Spring Data JPA - Querydsl + war + + This example demonstrates how you can create dynamic queries by using + Querydsl. + + + + + + io.spring.platform + platform-bom + 1.1.2.RELEASE + pom + import + + + + + + 1.8 + UTF-8 + true + false + 4.0.1.RELEASE + + + + + dev + + + integration-test + + false + true + + + + + + + + org.apache.commons + commons-lang3 + + + + org.slf4j + slf4j-api + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + + com.h2database + h2 + + + + com.zaxxer + HikariCP + + + + org.hibernate + hibernate-entitymanager + + + + org.jadira.usertype + usertype.extended + 3.2.0.GA + + + + com.mysema.querydsl + querydsl-jpa + + + + org.springframework.data + spring-data-jpa + + + + org.springframework + spring-aspects + + + org.springframework + spring-context-support + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet + jstl + + + org.springframework + spring-webmvc + + + org.hibernate + hibernate-validator + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + + + javax.el + javax.el-api + test + + + org.glassfish.web + el-impl + 2.2 + test + + + junit + junit + test + + + com.nitorcreations + junit-runners + 1.3 + test + + + org.assertj + assertj-core + 3.1.0 + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + info.solidsoft.mockito + mockito-java8 + 0.3.0 + test + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.security.version} + test + + + com.jayway.jsonpath + json-path + test + + + com.jayway.jsonpath + json-path-assert + 0.9.1 + test + + + com.github.springtestdbunit + spring-test-dbunit + 1.2.1 + test + + + org.dbunit + dbunit + 2.5.1 + test + + + junit + junit + + + + + + ROOT + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-integration-test-sources + generate-test-sources + + add-test-source + + + + src/integration-test/java + + + + + add-integration-test-resources + generate-test-resources + + add-test-resource + + + + + src/integration-test/resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${jdk.version} + ${jdk.version} + ${project.build.sourceEncoding} + + + + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/apt + com.mysema.query.apt.jpa.JPAAnnotationProcessor + + + + + + com.mysema.querydsl + querydsl-apt + 3.4.3 + + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + ROOT + false + + + frontend/build + / + false + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + ${skip.unit.tests} + + **/IT*.java + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18 + + + + integration-tests + + integration-test + verify + + + + ${skip.integration.tests} + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.2.10.v20150310 + + 0 + stop + 9999 + + + spring.profiles.active + application + + + + ${project.basedir}/target/ROOT.war + / + + ${project.basedir}/src/main/webapp + ${project.basedir}/frontend/build + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + + run + + + + + + + diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java new file mode 100644 index 0000000..3e294d5 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/TodoConstants.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * This class contains the constants that are used in our integration tests, DbUnit datasets, + * and the localization file. + * + * @author Petri Kainulainen + */ +public final class TodoConstants { + + public static class TodoEntries { + + public static class First { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "description"; + public static final Long ID = 1L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "title"; + } + + public static class Second { + + public static final String CREATED_BY_USER = "createdByUser"; + public static final String CREATION_TIME = "2014-12-24T14:13:28+03:00"; + public static final String DESCRIPTION = "tiscription"; + public static final Long ID = 2L; + public static final String MODIFIED_BY_USER = "modifiedByUser"; + public static final String MODIFICATION_TIME = "2014-12-25T14:13:28+03:00"; + public static final String TITLE = "First"; + + } + } + + public static final String SEARCH_TERM_DESCRIPTION_MATCHES = "esC"; + public static final String SEARCH_TERM_NO_MATCH = "NO MATCH"; + public static final String SEARCH_TERM_TITLE_MATCHES = "It"; + + public static final String UPDATED_DESCRIPTION = "updatedDescription"; + public static final String UPDATED_TITLE = "updatedTitle"; + + public static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 1"; + public static final String ERROR_MESSAGE_MISSING_TITLE = "The title cannot be empty"; + public static final String ERROR_MESSAGE_TOO_LONG_DESCRIPTION = "The maximum length of description is 500 characters"; + public static final String ERROR_MESSAGE_TOO_LONG_TITLE = "The maximum length of title is 100 characters"; + + /** + * Prevents instantiation + */ + private TodoConstants() {} +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java new file mode 100644 index 0000000..77cdb31 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/Users.java @@ -0,0 +1,31 @@ +package net.petrikainulainen.springdata.jpa; + +/** + * @author Petri Kainulainen + */ +public enum Users { + + USER("user", "password", "ROLE_USER"); + + private String password; + private String role; + private String username; + + Users(String username, String password, String role) { + this.password = password; + this.role = role; + this.username = username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } + + public String getUsername() { + return username; + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java new file mode 100644 index 0000000..d49bf35 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/todo/ITTodoRepositoryTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.Iterator; + +import static net.petrikainulainen.springdata.jpa.todo.TodoPredicates.titleOrDescriptionContainsIgnoreCase; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("todo-entries.xml") +public class ITTodoRepositoryTest { + + @Autowired + private TodoRepository repository; + + @Test + public void findBySearchTerm_SearchTermIsNull_ShouldReturnTwoTodoEntries() { + Iterable todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(null)); + + assertThat(todoEntries).hasSize(2); + + Iterator searchResults = todoEntries.iterator(); + + Todo firstTodoEntry = searchResults.next(); + assertThat(firstTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + + Todo secondTodoEntry = searchResults.next(); + assertThat(secondTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findBySearchTerm_SearchTermIsEmpty_ShouldReturnTwoTodoEntries() { + Iterable todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase("")); + + assertThat(todoEntries).hasSize(2); + + Iterator searchResults = todoEntries.iterator(); + + Todo firstTodoEntry = searchResults.next(); + assertThat(firstTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + + Todo secondTodoEntry = searchResults.next(); + assertThat(secondTodoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.Second.ID); + } + + @Test + public void findBySearchTerm_DescriptionOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + Iterable todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES)); + + assertThat(todoEntries).hasSize(1); + + Iterator searchResults = todoEntries.iterator(); + + Todo todoEntry = searchResults.next(); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } + + @Test + public void findBySearchTerm_NoMatch_ShouldReturnEmptyList() { + Iterable todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_NO_MATCH)); + assertThat(todoEntries).isEmpty(); + } + + @Test + public void findBySearchTerm_TitleOfOneTodoEntryMatches_ShouldReturnListThatHasOneTodoEntry() { + Iterable todoEntries = repository.findAll(titleOrDescriptionContainsIgnoreCase(TodoConstants.SEARCH_TERM_TITLE_MATCHES)); + + assertThat(todoEntries).hasSize(1); + + Iterator searchResults = todoEntries.iterator(); + + Todo todoEntry = searchResults.next(); + assertThat(todoEntry.getId()).isEqualTo(TodoConstants.TodoEntries.First.ID); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java new file mode 100644 index 0000000..af912d1 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ColumnSensingReplacementDataSetLoader.java @@ -0,0 +1,27 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.dataset.FlatXmlDataSetLoader; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ReplacementDataSet; +import org.springframework.core.io.Resource; +/** + * This class is a custom DbUnit data set loader that support flat XML data sets. This data set loader + * adds support for the extra features: + *
    + *
  • You can use the column sensing feature of DbUnit.
  • + *
  • You can specify that a column's value is null by using the string [null].
  • + *
+ * @author Petri Kainulainen + */ +public class ColumnSensingReplacementDataSetLoader extends FlatXmlDataSetLoader { + + @Override + protected IDataSet createDataSet(Resource resource) throws Exception { + return createReplacementDataSet(super.createDataSet(resource)); + } + private ReplacementDataSet createReplacementDataSet(IDataSet dataSet) { + ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet); + replacementDataSet.addReplacementObject("[null]", null); + return replacementDataSet; + } +} \ No newline at end of file diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java new file mode 100644 index 0000000..4360756 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/DbTestUtil.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Petri Kainulainen + */ +public final class DbTestUtil { + + private DbTestUtil() {} + + public static void resetAutoIncrementColumns(ApplicationContext applicationContext, + String... tableNames) throws SQLException { + DataSource dataSource = applicationContext.getBean(DataSource.class); + String resetSqlTemplate = getResetSqlTemplate(applicationContext); + try (Connection dbConnection = dataSource.getConnection()) { + //Create SQL statements that reset the auto increment columns and invoke + //the created SQL statements. + for (String resetSqlArgument: tableNames) { + try (Statement statement = dbConnection.createStatement()) { + String resetSql = String.format(resetSqlTemplate, resetSqlArgument); + statement.execute(resetSql); + } + } + } + } + + private static String getResetSqlTemplate(ApplicationContext applicationContext) { + //Read the SQL template from the properties file + Environment environment = applicationContext.getBean(Environment.class); + return environment.getRequiredProperty("test.reset.sql.template"); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java new file mode 100644 index 0000000..06b9c1c --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITCreateTest.java @@ -0,0 +1,251 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +@DatabaseSetup("no-todo-entries.xml") +public class ITCreateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + DbTestUtil.resetAutoIncrementColumns(webAppContext, "todos"); + + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void create_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotSaveTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusCreated() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isCreated()); + } + + @Test + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfCreatedTodoEntryAsJson() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.creationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @ExpectedDatabase(value = "create-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void create_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldSaveTodoEntry() throws Exception { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.TodoEntries.First.DESCRIPTION) + .title(TodoConstants.TodoEntries.First.TITLE) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java new file mode 100644 index 0000000..ed8aca6 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITDeleteTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITDeleteTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void delete_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfDeletedTodoEntry() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("delete-todo-entry-expected.xml") + @WithUserDetails("user") + public void delete_AsUser_WhenTodoEntryIsFound_ShouldDeleteTodoEntryFromDatabase() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .with(csrf()) + ); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java new file mode 100644 index 0000000..5781ca7 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindAllTest.java @@ -0,0 +1,97 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindAllTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void findAll_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findAll_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenTodoEntriesAreNotFound_ShouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findAll_AsUser_WhenOneTodoEntryIsFound_ShouldReturnInformationOfOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java new file mode 100644 index 0000000..393d146 --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindByIdTest.java @@ -0,0 +1,107 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITFindByIdTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void findById_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("$.message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void findById_AsUser_WhenTodoEntryIsFound_ShouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", TodoConstants.TodoEntries.First.ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java new file mode 100644 index 0000000..b08ad3c --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITFindBySearchTermTest.java @@ -0,0 +1,317 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +@DatabaseSetup("two-todo-entries.xml") +public class ITFindBySearchTermTest { + + private static final int FIRST_PAGE = 0; + private static final String FIRST_PAGE_STRING = "0"; + + private static final String PAGE_SIZE_STRING = "1"; + + private static final String SEARCH_TERM = "tIo"; + private static final int SECOND_PAGE = 1; + private static final String SECOND_PAGE_STRING = "1"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + /* Response status tests */ + @Test + public void findBySearchTerm_AsAnonymous_ShouldReturnHttpResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTodoEntriesAreFoundWithSearchTerm_ShouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(status().isOk()); + } + + + /* No results found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnEmptyPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.numberOfElements", is(0))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenNoTodoEntriesAreFoundWithSearchTerm_ShouldReturnAnPageThatHasZeroTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_NO_MATCH) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + /* One todo entry found */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenDescriptionOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_DESCRIPTION_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnPageThatHasOneTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTitleOfOneTodoEntryContainsTheGivenSearchTerm_ShouldReturnTheFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, TodoConstants.SEARCH_TERM_TITLE_MATCHES) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } + + /* Pagination tests */ + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldReturnFirstPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(FIRST_PAGE))) + .andExpect(jsonPath("$.first", is(true))) + .andExpect(jsonPath("$.last", is(false))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndFirstPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnSecondTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, FIRST_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.Second.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.Second.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.Second.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.Second.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.Second.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.Second.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.Second.TITLE))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.numberOfElements", is(1))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnPageThatHasTwoTotalElementsAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.totalElements", is(2))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldReturnLastPageJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.number", is(SECOND_PAGE))) + .andExpect(jsonPath("$.first", is(false))) + .andExpect(jsonPath("$.last", is(true))); + } + + @Test + @WithUserDetails("user") + public void findBySearchTerm_AsUser_WhenTwoTodoEntriesMatchesWithSearchTermAndSecondPageIsRequestedWithPageSizeOne_ShouldSortTodoEntriesByTitleAscAndReturnFirstTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, SECOND_PAGE_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content[0].createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(TodoConstants.TodoEntries.First.DESCRIPTION))) + .andExpect(jsonPath("$.content[0].id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(TodoConstants.TodoEntries.First.MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(TodoConstants.TodoEntries.First.MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TodoConstants.TodoEntries.First.TITLE))); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java new file mode 100644 index 0000000..fc0308b --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/ITUpdateTest.java @@ -0,0 +1,327 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.github.springtestdbunit.annotation.ExpectedDatabase; +import com.github.springtestdbunit.assertion.DatabaseAssertionMode; +import net.petrikainulainen.springdata.jpa.TodoConstants; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + TransactionalTestExecutionListener.class, + DbUnitTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +@WebAppConfiguration +public class ITUpdateTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + public void update_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isNotFound()); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(TodoConstants.ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND))); + } + + @Test + @DatabaseSetup("no-todo-entries.xml") + @ExpectedDatabase("no-todo-entries.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryIsNotFound_ShouldNotMakeAnyChangesToDatabase() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(TodoConstants.TodoEntries.First.ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(TodoConstants.ERROR_MESSAGE_MISSING_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasNoTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(TodoConstants.TodoEntries.First.ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + TodoConstants.ERROR_MESSAGE_TOO_LONG_DESCRIPTION, + TodoConstants.ERROR_MESSAGE_TOO_LONG_TITLE + ))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasTooLongTitleAndDescription_ShouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(TodoConstants.TodoEntries.First.ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnResponseStatusOk() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(status().isOk()); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(TodoConstants.TodoEntries.First.CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(TodoConstants.TodoEntries.First.CREATION_TIME))) + .andExpect(jsonPath("$.description", is(TodoConstants.UPDATED_DESCRIPTION))) + .andExpect(jsonPath("$.id", is(TodoConstants.TodoEntries.First.ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(Users.USER.getUsername()))) + .andExpect(jsonPath("$.modificationTime", is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))) + .andExpect(jsonPath("$.title", is(TodoConstants.UPDATED_TITLE))); + } + + @Test + @DatabaseSetup("one-todo-entry.xml") + @ExpectedDatabase(value = "update-todo-entry-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) + @WithUserDetails("user") + public void update_AsUser_WhenTodoEntryHasValidTitleAndDescription_ShouldUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(TodoConstants.UPDATED_DESCRIPTION) + .id(TodoConstants.TodoEntries.First.ID) + .title(TodoConstants.UPDATED_TITLE) + .build(); + + mockMvc.perform(put("/api/todo/{id}", TodoConstants.TodoEntries.First.ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + .with(csrf()) + ); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java new file mode 100644 index 0000000..e410ded --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITGetAuthenticatedUserTest.java @@ -0,0 +1,79 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import net.petrikainulainen.springdata.jpa.web.WebTestConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITGetAuthenticatedUserTest { + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void getAuthenticatedUser_AsAnonymous_ShouldReturnResponseStatusUnauthorized() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("user") + public void getAuthenticatedUser_AsUser_ShouldReturnUserInformationAsJSON() throws Exception { + mockMvc.perform(get("/api/authenticated-user")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.username", is("user"))) + .andExpect(jsonPath("$.role", is(UserRole.ROLE_USER.name()))); + } +} diff --git a/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java new file mode 100644 index 0000000..a7d93ff --- /dev/null +++ b/querydsl/src/integration-test/java/net/petrikainulainen/springdata/jpa/web/security/ITLoginTest.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import net.petrikainulainen.springdata.jpa.Users; +import net.petrikainulainen.springdata.jpa.config.ExampleApplicationContext; +import net.petrikainulainen.springdata.jpa.config.Profiles; +import net.petrikainulainen.springdata.jpa.web.ColumnSensingReplacementDataSetLoader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.sql.SQLException; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles(Profiles.INTEGRATION_TEST) +@ContextConfiguration(classes = {ExampleApplicationContext.class}) +@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class +}) +@WebAppConfiguration +public class ITLoginTest { + + private static final String INVALID_PASSWORD = "invalidPassword"; + private static final String INVALID_USERNAME = "invalidUsername"; + + private static final String PARAM_NAME_PASSWORD = "password"; + private static final String PARAM_NAME_USERNAME = "username"; + + @Autowired + private WebApplicationContext webAppContext; + + private MockMvc mockMvc; + + @Before + public void setUp() throws SQLException { + mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void logIn_WhenUsernameIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, INVALID_USERNAME) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenPasswordIsIncorrect_ShouldReturnResponseStatusForbidden() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, INVALID_PASSWORD) + .with(csrf()) + ) + .andExpect(status().isForbidden()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldReturnResponseStatusFound() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(status().isFound()); + } + + @Test + public void logIn_WhenUsernameAndPasswordAreCorrect_ShouldRedirectClientToControllerMethodThatReturnsAuthenticatedUser() throws Exception { + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param(PARAM_NAME_USERNAME, Users.USER.getUsername()) + .param(PARAM_NAME_PASSWORD, Users.USER.getPassword()) + .with(csrf()) + ) + .andExpect(redirectedUrl("/api/authenticated-user")); + } +} diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml new file mode 100644 index 0000000..5a18c38 --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/todo/todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml new file mode 100644 index 0000000..12e0c00 --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/create-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/delete-todo-entry-expected.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml new file mode 100644 index 0000000..c180adb --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/no-todo-entries.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml new file mode 100644 index 0000000..50193f2 --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/one-todo-entry.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml new file mode 100644 index 0000000..0c1e6bc --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/two-todo-entries.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml new file mode 100644 index 0000000..fbb3e27 --- /dev/null +++ b/querydsl/src/integration-test/resources/net/petrikainulainen/springdata/jpa/web/update-todo-entry-expected.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/querydsl/src/main/ant/build.xml b/querydsl/src/main/ant/build.xml new file mode 100644 index 0000000..90d4c18 --- /dev/null +++ b/querydsl/src/main/ant/build.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java new file mode 100644 index 0000000..6a9566b --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/AuditingDateTimeProvider.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * This class obtains the current time by using a {@link DateTimeService} + * object. The reason for this is that we can use a different implementation in our integration tests. + * + * In other words: + *
    + *
  • + * Our application always returns the correct time because it uses the + * {@link CurrentTimeDateTimeService} class. + *
  • + *
  • + * When our integration tests are running, we can return a constant time which gives us the possibility + * to assert the creation and modification times saved to the database. + *
  • + *
+ * + * @author Petri Kainulainen + */ +public class AuditingDateTimeProvider implements DateTimeProvider { + + private final DateTimeService dateTimeService; + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Calendar getNow() { + return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java new file mode 100644 index 0000000..424e1d4 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/ConstantDateTimeService.java @@ -0,0 +1,47 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * This class is used in our integration tests and it always returns the + * same time. This gives us the possibility to verify that the correct + * timestamps are saved to the database. + * + * @author Petri Kainulainen + */ +public class ConstantDateTimeService implements DateTimeService { + + public static final String CURRENT_DATE_AND_TIME = getConstantDateAndTime(); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConstantDateTimeService.class); + + private static String getConstantDateAndTime() { + return "2015-07-19T12:52:28" + + getSystemZoneOffset() + + getSystemZoneId(); + } + + private static String getSystemZoneOffset() { + return ZonedDateTime.now().getOffset().toString(); + } + + private static String getSystemZoneId() { + return "[" + ZoneId.systemDefault().toString() + "]"; + } + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime constantDateAndTime = ZonedDateTime.from(FORMATTER.parse(CURRENT_DATE_AND_TIME)); + + LOGGER.info("Returning constant date and time: {}", constantDateAndTime); + + return constantDateAndTime; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java new file mode 100644 index 0000000..2812fb0 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/CurrentTimeDateTimeService.java @@ -0,0 +1,25 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +/** + * This class returns the current time. + * + * @author Petri Kainulainen + */ +public class CurrentTimeDateTimeService implements DateTimeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CurrentTimeDateTimeService.class); + + @Override + public ZonedDateTime getCurrentDateAndTime() { + ZonedDateTime currentDateAndTime = ZonedDateTime.now(); + + LOGGER.info("Returning current date and time: {}", currentDateAndTime); + + return currentDateAndTime; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java new file mode 100644 index 0000000..a1e1a11 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/DateTimeService.java @@ -0,0 +1,18 @@ +package net.petrikainulainen.springdata.jpa.common; + +import java.time.ZonedDateTime; + +/** + * This interface defines the methods used to get the current + * date and time. + * + * @author Petri Kainulainen + */ +public interface DateTimeService { + + /** + * Returns the current date and time. + * @return + */ + ZonedDateTime getCurrentDateAndTime(); +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java new file mode 100644 index 0000000..46f2849 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/FrontendLoaderController.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * This controller is responsible of starting the frontend application. + * @author Petri Kainulainen + */ +@Controller +public class FrontendLoaderController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FrontendLoaderController.class); + + private static final String FRONTEND_APPLICATION_VIEW = "frontend/client"; + + /** + * Starts the AngularJS application. + * @return + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public String startAngularJSApplication() { + LOGGER.debug("Starting frontend single page application."); + return FRONTEND_APPLICATION_VIEW; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java new file mode 100644 index 0000000..d3cf557 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/PreCondition.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.common; + +/** + * This class provides static utility methods that are used to ensure that a constructor or a method was invoked properly. + * These methods throw an exception if the specified precondition is violated. + * + * This class selects the thrown exception by using the guideline given in Effective Java by Joshua Bloch (Item 60). + * + * @author Petri Kainulainen + */ +public final class PreCondition { + + private PreCondition() {} + + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessageTemplate The template that is used to construct the message of the exception thrown if the + * inspected exception is false. The template must use the syntax that is supported + * by the {@link java.lang.String#format(String, Object...)} method. + * @param errorMessageArguments The arguments that are used when the message of the thrown exception is constructed. + * @throws java.lang.IllegalArgumentException if the inspected exception is false. + */ + public static void isTrue(boolean expression, String errorMessageTemplate, Object... errorMessageArguments) { + isTrue(expression, String.format(errorMessageTemplate, errorMessageArguments)); + } + /** + * Ensures that the expression given as a method parameter is true. + * @param expression The inspected expression. + * @param errorMessage The error message that is passed forward to the exception that is thrown + * if the expression is false. + * @throws java.lang.IllegalArgumentException if the inspected expression is false. + */ + public static void isTrue(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the string given as a method parameter is not empty. + * @param string The inspected string. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the string is empty. + * @throws java.lang.IllegalArgumentException if the inspected string is empty. + */ + public static void notEmpty(String string, String errorMessage) { + if (string.isEmpty()) { + throw new IllegalArgumentException(errorMessage); + } + } + /** + * Ensures that the object given as a method parameter is not null. + * @param reference A reference to the inspected object. + * @param errorMessage The error message that is passed forward to the exception that is thrown if + * the object given as a method parameter is null. + * @throws java.lang.NullPointerException If the object given as a method parameter is null. + */ + public static void notNull(Object reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java new file mode 100644 index 0000000..ed511d8 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/common/UsernameAuditorAware.java @@ -0,0 +1,34 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/** + * This component returns the username of the authenticated user. + * + * @author Petri Kainulainen + */ +public class UsernameAuditorAware implements AuditorAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(UsernameAuditorAware.class); + + @Override + public String getCurrentAuditor() { + LOGGER.debug("Getting the username of authenticated user."); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + LOGGER.debug("Current user is anonymous. Returning null."); + return null; + } + + String username = ((User) authentication.getPrincipal()).getUsername(); + LOGGER.debug("Returning username: {}", username); + + return username; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java new file mode 100644 index 0000000..0f922d8 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/ExampleApplicationContext.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.common.ConstantDateTimeService; +import net.petrikainulainen.springdata.jpa.common.CurrentTimeDateTimeService; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * @author Petri Kainulainen + */ +@Configuration +@ComponentScan("net.petrikainulainen.springdata.jpa") +@Import({WebMvcContext.class, PersistenceContext.class, SecurityContext.class}) +public class ExampleApplicationContext { + + private static final String MESSAGE_SOURCE_BASE_NAME = "i18n/messages"; + + /** + * These static classes are required because it makes it possible to use + * different properties files for every Spring profile. + * + * See: This StackOverflow answer for more details. + */ + @Profile(Profiles.APPLICATION) + @Configuration + @PropertySource("classpath:application.properties") + static class ApplicationProperties {} + + @Profile(Profiles.APPLICATION) + @Bean + DateTimeService currentTimeDateTimeService() { + return new CurrentTimeDateTimeService(); + } + + @Profile(Profiles.INTEGRATION_TEST) + @Configuration + @PropertySource("classpath:integration-test.properties") + static class IntegrationTestProperties {} + + @Profile(Profiles.INTEGRATION_TEST) + @Bean + DateTimeService constantDateTimeService() { + return new ConstantDateTimeService(); + } + + @Bean + MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(MESSAGE_SOURCE_BASE_NAME); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } + + @Bean + PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java new file mode 100644 index 0000000..78a9a36 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/PersistenceContext.java @@ -0,0 +1,136 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.petrikainulainen.springdata.jpa.common.AuditingDateTimeProvider; +import net.petrikainulainen.springdata.jpa.common.DateTimeService; +import net.petrikainulainen.springdata.jpa.common.UsernameAuditorAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +/** + * This configuration class configures the persistence layer of our example application and + * enables annotation driven transaction management. + * + * This configuration is put to a single class because this way we can write integration + * tests for our persistence layer by using the configuration used by our example + * application. In other words, we can ensure that the persistence layer of our application + * works as expected. + * + * @author Petri Kainulainen + */ +@Configuration +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") +@EnableJpaRepositories(basePackages = { + "net.petrikainulainen.springdata.jpa.todo" +}) +@EnableTransactionManagement +@EnableSpringDataWebSupport +class PersistenceContext { + private static final String[] ENTITY_PACKAGES = { + "net.petrikainulainen.springdata.jpa.todo" + }; + + private static final String PROPERTY_NAME_DB_DRIVER_CLASS = "db.driver"; + private static final String PROPERTY_NAME_DB_PASSWORD = "db.password"; + private static final String PROPERTY_NAME_DB_URL = "db.url"; + private static final String PROPERTY_NAME_DB_USER = "db.username"; + private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; + private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; + private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; + private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; + private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; + + @Bean + AuditorAware auditorProvider() { + return new UsernameAuditorAware(); + } + + @Bean + DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { + return new AuditingDateTimeProvider(dateTimeService); + } + + /** + * Creates and configures the HikariCP datasource bean. + * @param env The runtime environment of our application. + * @return + */ + @Bean(destroyMethod = "close") + DataSource dataSource(Environment env) { + HikariConfig dataSourceConfig = new HikariConfig(); + dataSourceConfig.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DB_DRIVER_CLASS)); + dataSourceConfig.setJdbcUrl(env.getRequiredProperty(PROPERTY_NAME_DB_URL)); + dataSourceConfig.setUsername(env.getRequiredProperty(PROPERTY_NAME_DB_USER)); + dataSourceConfig.setPassword(env.getRequiredProperty(PROPERTY_NAME_DB_PASSWORD)); + + return new HikariDataSource(dataSourceConfig); + } + + /** + * Creates the bean that creates the JPA entity manager factory. + * @param dataSource The datasource that provides the database connections. + * @param env The runtime environment of our application. + * @return + */ + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + entityManagerFactoryBean.setPackagesToScan(ENTITY_PACKAGES); + + Properties jpaProperties = new Properties(); + + //Configures the used database dialect. This allows Hibernate to create SQL + //that is optimized for the used database. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); + + //Specifies the action that is invoked to the database when the Hibernate + //SessionFactory is created or closed. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); + + //Configures the naming strategy that is used when Hibernate creates + //new database objects and schema elements + jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); + + //If the value of this property is true, Hibernate writes all SQL + //statements to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); + + //If the value of this property is true, Hibernate will use prettyprint + //when it writes SQL to the console. + jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); + + entityManagerFactoryBean.setJpaProperties(jpaProperties); + + return entityManagerFactoryBean; + } + + /** + * Creates the transaction manager bean that integrates the used JPA provider with the + * Spring transaction mechanism. + * @param entityManagerFactory The used JPA entity manager factory. + * @return + */ + @Bean + JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java new file mode 100644 index 0000000..bda9711 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/Profiles.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.config; + +/** + * This class defines the Spring profiles used in the project. The idea behind this class + * is that it helps us to avoid typos when we are using these profiles. At the moment + * there are two profiles which are described in the following: + *
    + *
  • The APPLICATION profile is used when we run our example application.
  • + *
  • The INTEGRATION_TEST profile is used when we run the integration tests of our example application.
  • + *
+ * + * @author Petri Kainulainen + */ +public class Profiles { + public static final String APPLICATION = "application"; + public static final String INTEGRATION_TEST = "integrationtest"; + /** + * Prevent instantiation. + */ + private Profiles() {} +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java new file mode 100644 index 0000000..8aa95e4 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/SecurityContext.java @@ -0,0 +1,99 @@ +package net.petrikainulainen.springdata.jpa.config; + +import net.petrikainulainen.springdata.jpa.web.security.CsrfHeaderFilter; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationEntryPoint; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationFailureHandler; +import net.petrikainulainen.springdata.jpa.web.security.RestAuthenticationSuccessHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CsrfFilter; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebSecurity +class SecurityContext extends WebSecurityConfigurerAdapter { + + @Bean + AuthenticationEntryPoint authenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + AuthenticationFailureHandler authenticationFailureHandler() { + return new RestAuthenticationFailureHandler(); + } + + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler() { + return new RestAuthenticationSuccessHandler(); + } + + @Bean + protected UserDetailsService userDetailsService() { + return super.userDetailsService(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + //Use the custom authentication entry point. + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint()) + .and() + //Configure form login. + .formLogin() + .loginProcessingUrl("/api/login") + .failureHandler(authenticationFailureHandler()) + .successHandler(authenticationSuccessHandler()) + .permitAll() + .and() + //Configure logout function. + .logout() + .deleteCookies("JSESSIONID") + .logoutUrl("/api/logout") + .logoutSuccessUrl("/") + .and() + //Configure url based authorization + .authorizeRequests() + .antMatchers( + "/", + "/api/csrf" + ).permitAll() + .anyRequest().hasRole("USER") + .and() + .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + //Spring Security ignores request to static resources such as CSS or JS files. + .ignoring() + .antMatchers( + "/favicon.ico", + "/css/**", + "/i18n/**", + "/js/**" + ); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java new file mode 100644 index 0000000..f1861a6 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebAppConfig.java @@ -0,0 +1,66 @@ +package net.petrikainulainen.springdata.jpa.config; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import java.util.EnumSet; + +/** + * @author Petri Kainulainen + */ +public class WebAppConfig implements WebApplicationInitializer { + private static final String CHARACTER_ENCODING_FILTER_ENCODING = "UTF-8"; + private static final String CHARACTER_ENCODING_FILTER_NAME = "characterEncoding"; + private static final String CHARACTER_ENCODING_FILTER_URL_PATTERN = "/*"; + + private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; + private static final String DISPATCHER_SERVLET_MAPPING = "/"; + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(ExampleApplicationContext.class); + + //XmlWebApplicationContext rootContext = new XmlWebApplicationContext(); + //rootContext.setConfigLocation("classpath:applicationContext.xml"); + + configureDispatcherServlet(servletContext, rootContext); + EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + + configureCharacterEncodingFilter(servletContext, dispatcherTypes); + configureSpringSecurityFilter(servletContext, dispatcherTypes); + servletContext.addListener(new ContextLoaderListener(rootContext)); + } + + private void configureDispatcherServlet(ServletContext servletContext, WebApplicationContext rootContext) { + ServletRegistration.Dynamic dispatcher = servletContext.addServlet( + DISPATCHER_SERVLET_NAME, + new DispatcherServlet(rootContext) + ); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); + } + + private void configureCharacterEncodingFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + characterEncodingFilter.setEncoding(CHARACTER_ENCODING_FILTER_ENCODING); + characterEncodingFilter.setForceEncoding(true); + FilterRegistration.Dynamic characterEncoding = servletContext.addFilter(CHARACTER_ENCODING_FILTER_NAME, characterEncodingFilter); + characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, CHARACTER_ENCODING_FILTER_URL_PATTERN); + } + + private void configureSpringSecurityFilter(ServletContext servletContext, EnumSet dispatcherTypes) { + FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); + security.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java new file mode 100644 index 0000000..c016860 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/config/WebMvcContext.java @@ -0,0 +1,48 @@ +package net.petrikainulainen.springdata.jpa.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; + +/** + * @author Petri Kainulainen + */ +@Configuration +@EnableWebMvc +class WebMvcContext extends WebMvcConfigurerAdapter { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + + @Override + public void configureMessageConverters(List> converters) { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + + converters.add(converter); + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp("/WEB-INF/jsp/", ".jsp"); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java new file mode 100644 index 0000000..3591308 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchService.java @@ -0,0 +1,44 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static net.petrikainulainen.springdata.jpa.todo.TodoPredicates.titleOrDescriptionContainsIgnoreCase; + + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoSearchService implements TodoSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoSearchService.class); + + private final TodoRepository repository; + + @Autowired + public RepositoryTodoSearchService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + @Override + public Page findBySearchTerm(String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = repository.findAll(titleOrDescriptionContainsIgnoreCase(searchTerm), pageRequest); + + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return TodoMapper.mapEntityPageIntoDTOPage(pageRequest, searchResultPage); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java new file mode 100644 index 0000000..f59be0d --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoService.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * @author Petri Kainulainen + */ +@Service +final class RepositoryTodoService implements TodoCrudService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTodoService.class); + + private final TodoRepository repository; + + @Autowired + RepositoryTodoService(TodoRepository repository) { + this.repository = repository; + } + + @Transactional + @Override + public TodoDTO create(TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + Todo created = Todo.getBuilder() + .description(newTodoEntry.getDescription()) + .title(newTodoEntry.getTitle()) + .build(); + + created = repository.save(created); + LOGGER.info("Created a new todo entry: {}", created); + + return TodoMapper.mapEntityIntoDTO(created); + } + + @Transactional + @Override + public TodoDTO delete(Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + Todo deleted = findTodoEntryById(id); + LOGGER.debug("Found todo entry: {}", deleted); + + repository.delete(deleted); + LOGGER.info("Deleted todo entry: {}", deleted); + + return TodoMapper.mapEntityIntoDTO(deleted); + } + + @Transactional(readOnly = true) + @Override + public List findAll() { + LOGGER.info("Finding all todo entries."); + + List todoEntries = repository.findAll(); + + LOGGER.info("Found {} todo entries", todoEntries.size()); + + return TodoMapper.mapEntitiesIntoDTOs(todoEntries); + } + + @Transactional(readOnly = true) + @Override + public TodoDTO findById(Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + Todo todoEntry = findTodoEntryById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return TodoMapper.mapEntityIntoDTO(todoEntry); + } + + @Transactional + @Override + public TodoDTO update(TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + Todo updated = findTodoEntryById(updatedTodoEntry.getId()); + updated.update(updatedTodoEntry.getTitle(), updatedTodoEntry.getDescription()); + + //We need to flush the changes or otherwise the returned object + //doesn't contain the updated audit information. + repository.flush(); + + LOGGER.info("Updated the information of the todo entry: {}", updated); + + return TodoMapper.mapEntityIntoDTO(updated); + } + + private Todo findTodoEntryById(Long id) { + Optional todoResult = repository.findOne(id); + return todoResult.orElseThrow(() -> new TodoNotFoundException(id)); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java new file mode 100644 index 0000000..4eccac6 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/Todo.java @@ -0,0 +1,181 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.annotations.Type; +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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Version; +import java.time.ZonedDateTime; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.isTrue; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This entity class contains the information of a single todo entry + * and the methods that are used to create new todo entries and to modify + * the information of an existing todo entry. + * + * @author Petri Kainulainen + */ +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "todos") +final class Todo { + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(name = "created_by_user", nullable = false) + @CreatedBy + private String createdByUser; + + @Column(name = "creation_time", nullable = false) + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @CreatedDate + private ZonedDateTime creationTime; + + @Column(name = "description", length = MAX_LENGTH_DESCRIPTION) + private String description; + + @Column(name = "modified_by_user", nullable = false) + @LastModifiedBy + private String modifiedByUser; + + @Column(name = "modification_time") + @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") + @LastModifiedDate + private ZonedDateTime modificationTime; + + @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE) + private String title; + + @Version + private long version; + + /** + * Required by Hibernate. + */ + private Todo() {} + + private Todo(Builder builder) { + this.title = builder.title; + this.description = builder.description; + } + + static Builder getBuilder() { + return new Builder(); + } + + Long getId() { + return id; + } + + String getCreatedByUser() { + return createdByUser; + } + + ZonedDateTime getCreationTime() { + return creationTime; + } + + String getDescription() { + return description; + } + + String getModifiedByUser() { + return modifiedByUser; + } + + ZonedDateTime getModificationTime() { + return modificationTime; + } + + String getTitle() { + return title; + } + + long getVersion() { + return version; + } + + void update(String newTitle, String newDescription) { + requireValidTitleAndDescription(newTitle, newDescription); + + this.title = newTitle; + this.description = newDescription; + } + + private void requireValidTitleAndDescription(String title, String description) { + notNull(title, "Title cannot be null."); + notEmpty(title, "Title cannot be empty."); + isTrue(title.length() <= MAX_LENGTH_TITLE, + "The maximum length of the title is <%d> characters.", + MAX_LENGTH_TITLE + ); + + isTrue((description == null) || (description.length() <= MAX_LENGTH_DESCRIPTION), + "The maximum length of the description is <%d> characters.", + MAX_LENGTH_DESCRIPTION + ); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .append("version", this.version) + .toString(); + } + + /** + * This entity is so simple that you don't really need to use the builder pattern + * (use a constructor instead). I use the builder pattern here because it makes + * the code a bit more easier to read. + */ + static class Builder { + private String description; + private String title; + + private Builder() {} + + Builder description(String description) { + this.description = description; + return this; + } + + Builder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = new Todo(this); + + build.requireValidTitleAndDescription(build.getTitle(), build.getDescription()); + + return build; + } + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java new file mode 100644 index 0000000..9e6fc09 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoCrudService.java @@ -0,0 +1,49 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.util.List; + +/** + * This service provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +public interface TodoCrudService { + + /** + * Creates a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + TodoDTO create(TodoDTO newTodoEntry); + + /** + * Deletes a todo entry from the database. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + TodoDTO delete(Long id); + + /** + * Finds all todo entries that are saved to the database. + * @return + */ + List findAll(); + + /** + * Finds a todo entry by using the id given as a method parameter. + * @param id The id of the wanted todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO findById(Long id); + + /** + * Updates the information of an existing information. + * @param updatedTodoEntry The new information of an existing todo entry. + * @return The information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found with the given id. + */ + TodoDTO update(TodoDTO updatedTodoEntry); +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java new file mode 100644 index 0000000..7eea8d2 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoDTO.java @@ -0,0 +1,101 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Size; +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTO { + + private String createdByUser; + + private ZonedDateTime creationTime; + + @Size(max = Todo.MAX_LENGTH_DESCRIPTION) + private String description; + + private Long id; + + private String modifiedByUser; + + private ZonedDateTime modificationTime; + + @NotEmpty + @Size(max = Todo.MAX_LENGTH_TITLE) + private String title; + + public TodoDTO() {} + + public String getCreatedByUser() { + return createdByUser; + } + + public ZonedDateTime getCreationTime() { + return creationTime; + } + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + public String getModifiedByUser() { + return modifiedByUser; + } + + public ZonedDateTime getModificationTime() { + return modificationTime; + } + + public String getTitle() { + return title; + } + + public void setCreatedByUser(String createdByUser) { + this.createdByUser = createdByUser; + } + + public void setCreationTime(ZonedDateTime creationTime) { + this.creationTime = creationTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setId(Long id) { + this.id = id; + } + + public void setModifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + + public void setModificationTime(ZonedDateTime modificationTime) { + this.modificationTime = modificationTime; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("createdByUser", this.createdByUser) + .append("creationTime", this.creationTime) + .append("description", this.description) + .append("id", this.id) + .append("modifiedByUser", this.modifiedByUser) + .append("modificationTime", this.modificationTime) + .append("title", this.title) + .toString(); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java new file mode 100644 index 0000000..0ccb5e2 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java @@ -0,0 +1,63 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is a mapper class that is used to transform {@link Todo} objects + * into {@link TodoDTO} objects. + * @author Petri Kainulainen + */ +final class TodoMapper { + + /** + * Transforms the list of {@link Todo} objects given as a method parameter + * into a list of {@link TodoDTO} objects and returns the created list. + * + * @param entities + * @return + */ + static List mapEntitiesIntoDTOs(Iterable entities) { + List dtos = new ArrayList<>(); + + entities.forEach(e -> dtos.add(mapEntityIntoDTO(e))); + + return dtos; + } + + /** + * Transforms the {@link Todo} object given as a method parameter into a + * {@link TodoDTO} object and returns the created object. + * + * @param entity + * @return + */ + static TodoDTO mapEntityIntoDTO(Todo entity) { + TodoDTO dto = new TodoDTO(); + + dto.setCreatedByUser(entity.getCreatedByUser()); + dto.setCreationTime(entity.getCreationTime()); + dto.setDescription(entity.getDescription()); + dto.setId(entity.getId()); + dto.setModifiedByUser(entity.getModifiedByUser()); + dto.setModificationTime(entity.getModificationTime()); + dto.setTitle(entity.getTitle()); + + return dto; + } + + /** + * Transforms {@code Page} objects into {@code Page} objects. + * @param pageRequest The information of the requested page. + * @param source The {@code Page} object. + * @return The created {@code Page} object. + */ + static Page mapEntityPageIntoDTOPage(Pageable pageRequest, Page source) { + List dtos = mapEntitiesIntoDTOs(source.getContent()); + return new PageImpl<>(dtos, pageRequest, source.getTotalElements()); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java new file mode 100644 index 0000000..63f6948 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoNotFoundException.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +/** + * This exception is thrown when a todo entry is not found by + * using the given id. + * + * @author Petri Kainulainen + */ +public class TodoNotFoundException extends RuntimeException { + + private final Long id; + + public TodoNotFoundException(Long id) { + super(); + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoPredicates.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoPredicates.java new file mode 100644 index 0000000..443e8d2 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoPredicates.java @@ -0,0 +1,38 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.mysema.query.types.Predicate; + +/** + * This predicate builder class provides static methods that are used + * to create Predicate objects which specify + * the search criteria of dynamic database queries. + * + * @author Petri Kainulainen + */ +final class TodoPredicates { + + /** + * Prevent instantiation + */ + private TodoPredicates() {} + + /** + * Creates the search criteria that returns all todo entries whose title or description + * contains the given search term. The search is case insensitive. + * + * If the search term is null or empty, the created search criteria will return + * all todo entries. + * + * @param searchTerm The used search term. + * @return + */ + static Predicate titleOrDescriptionContainsIgnoreCase(String searchTerm) { + if (searchTerm == null || searchTerm.isEmpty()) { + return QTodo.todo.isNotNull(); + } + else { + return QTodo.todo.description.containsIgnoreCase(searchTerm) + .or(QTodo.todo.title.containsIgnoreCase(searchTerm)); + } + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java new file mode 100644 index 0000000..f471660 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoRepository.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * This repository provides CRUD operations for {@link net.petrikainulainen.springdata.jpa.todo.Todo} + * objects. + * + * @author Petri Kainulainen + */ +interface TodoRepository extends Repository, QueryDslPredicateExecutor { + + void delete(Todo deleted); + + List findAll(); + + Optional findOne(Long id); + + void flush(); + + Todo save(Todo persisted); +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java new file mode 100644 index 0000000..41212eb --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoSearchService.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * This service provides finder methods for {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +public interface TodoSearchService { + + /** + * Finds todo entries whose title or description contains the given search term. + * This search is case insensitive. + * @param searchTerm The search term. + * @param pageRequest The information of the requested page. + * @return + */ + Page findBySearchTerm(String searchTerm, Pageable pageRequest); +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java new file mode 100644 index 0000000..5205657 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoController.java @@ -0,0 +1,116 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * This controller provides the public API that is used to perform + * CRUD operations for todo entries. + * + * @author Petri Kainulainen + */ +@RestController +@RequestMapping("/api/todo") +final class TodoController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoController.class); + + private final TodoCrudService crudService; + + @Autowired + TodoController(TodoCrudService crudService) { + this.crudService = crudService; + } + + /** + * Create a new todo entry. + * @param newTodoEntry The information of the created todo entry. + * @return The information of the created todo entry. + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + TodoDTO create(@RequestBody @Valid TodoDTO newTodoEntry) { + LOGGER.info("Creating a new todo entry by using information: {}", newTodoEntry); + + TodoDTO created = crudService.create(newTodoEntry); + LOGGER.info("Created a new todo entry: {}", created); + + return created; + } + + /** + * Deletes a todo entry. + * @param id The id of the deleted todo entry. + * @return The information of the deleted todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if the deleted todo entry is not found. + */ + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + public TodoDTO delete(@PathVariable("id") Long id) { + LOGGER.info("Deleting a todo entry with id: {}", id); + + TodoDTO deleted = crudService.delete(id); + LOGGER.info("Deleted the todo entry: {}", deleted); + + return deleted; + } + + /** + * Finds all todo entries. + * + * @return The information of all todo entries. + */ + @RequestMapping(method = RequestMethod.GET) + List findAll() { + LOGGER.info("Finding all todo entries"); + + List todoEntries = crudService.findAll(); + LOGGER.info("Found {} todo entries.", todoEntries.size()); + + return todoEntries; + } + + /** + * Finds a single todo entry. + * @param id The id of the requested todo entry. + * @return The information of the requested todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.GET) + TodoDTO findById(@PathVariable("id") Long id) { + LOGGER.info("Finding todo entry by using id: {}", id); + + TodoDTO todoEntry = crudService.findById(id); + LOGGER.info("Found todo entry: {}", todoEntry); + + return todoEntry; + } + + /** + * Updates the information of an existing todo entry. + * @param updatedTodoEntry The new information of the updated todo entry. + * @return The updated information of the updated todo entry. + * @throws net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException if no todo entry is found by using the given id. + */ + @RequestMapping(value = "{id}", method = RequestMethod.PUT) + TodoDTO update(@RequestBody @Valid TodoDTO updatedTodoEntry) { + LOGGER.info("Updating the information of a todo entry by using information: {}", updatedTodoEntry); + + TodoDTO updated = crudService.update(updatedTodoEntry); + LOGGER.info("Updated the information of the todo entrY: {}", updated); + + return updated; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java new file mode 100644 index 0000000..b6c95d9 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/TodoSearchController.java @@ -0,0 +1,53 @@ +package net.petrikainulainen.springdata.jpa.web; + +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to find todo entries by using + * different search criteria. + * + * @author Petri Kainulainen + */ +@RestController +final class TodoSearchController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TodoSearchController.class); + + private final TodoSearchService searchService; + + @Autowired + public TodoSearchController(TodoSearchService searchService) { + this.searchService = searchService; + } + + /** + * Finds todo entries whose title or description contains the given search term. This + * search is case insensitive. + * @param searchTerm The used search term. + * @param pageRequest The information of the requested page + * @return + */ + @RequestMapping(value = "/api/todo/search", method = RequestMethod.GET) + public Page findBySearchTerm(@RequestParam("searchTerm") String searchTerm, Pageable pageRequest) { + LOGGER.info("Finding todo entries by search term: {} and page request: {}", searchTerm, pageRequest); + + Page searchResultPage = searchService.findBySearchTerm(searchTerm, pageRequest); + LOGGER.info("Found {} todo entries. Returned page {} contains {} todo entries", + searchResultPage.getTotalElements(), + searchResultPage.getNumber(), + searchResultPage.getNumberOfElements() + ); + + return searchResultPage; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java new file mode 100644 index 0000000..b02059e --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of an error that occurred when the API tried + * to perform the operation requested by the client. + * + * @author Petri Kainulainen + */ +final class ErrorDTO { + + private final String code; + private final String message; + + ErrorDTO(String code, String message) { + notNull(code, "Code cannot be null."); + notEmpty(code, "Code cannot be empty."); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty"); + + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java new file mode 100644 index 0000000..44234a5 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notEmpty; +import static net.petrikainulainen.springdata.jpa.common.PreCondition.notNull; + +/** + * This class contains the information of a single field error. + * + * @author Petri Kainulainen + */ +final class FieldErrorDTO { + + private final String field; + + private final String message; + + FieldErrorDTO(String field, String message) { + notNull(field, "Field cannot be null."); + notEmpty(field, "Field cannot be empty"); + + notNull(message, "Message cannot be null."); + notEmpty(message, "Message cannot be empty."); + + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java new file mode 100644 index 0000000..5ad9e9b --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandler.java @@ -0,0 +1,106 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; +import java.util.Locale; + +/** + * This class handles the exceptions thrown by our REST API. + * + * @author Petri Kainulainen + */ +@ControllerAdvice +public final class RestErrorHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class); + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + + private final MessageSource messageSource; + + @Autowired + public RestErrorHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * Processes an error that occurs when the requested todo entry is not found. + * @param ex The exception that was thrown when the todo entry was not found. + * @param currentLocale The current locale. + * @return An error object that contains the error code and message. + */ + @ExceptionHandler(TodoNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + ErrorDTO handleTodoEntryNotFound(TodoNotFoundException ex, Locale currentLocale) { + LOGGER.error("Todo entry was not found by using id: {}", ex.getId()); + + MessageSourceResolvable errorMessageRequest = createSingleErrorMessageRequest( + ERROR_CODE_TODO_ENTRY_NOT_FOUND, + ex.getId() + ); + + String errorMessage = messageSource.getMessage(errorMessageRequest, currentLocale); + return new ErrorDTO(HttpStatus.NOT_FOUND.name(), errorMessage); + } + + private DefaultMessageSourceResolvable createSingleErrorMessageRequest(String errorMessageCode, Object... params) { + return new DefaultMessageSourceResolvable(new String[] {errorMessageCode}, params); + } + + /** + * Processes an error that occurs when the validation of an object fails. + * + * @param ex The exception that was thrown when the validation failed. + * @return An error object that describes all validation errors. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ValidationErrorDTO handleValidationErrors(MethodArgumentNotValidException ex, Locale currentLocale) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result.getFieldErrors(); + LOGGER.error("Found {} validation errors", fieldErrors.size()); + + return constructValidationErrors(fieldErrors, currentLocale); + } + + private ValidationErrorDTO constructValidationErrors(List fieldErrors, Locale currentLocale) { + ValidationErrorDTO dto = new ValidationErrorDTO(); + + for (FieldError fieldError: fieldErrors) { + String localizedErrorMessage = getValidationErrorMessage(fieldError, currentLocale); + dto.addFieldError(fieldError.getField(), localizedErrorMessage); + } + + return dto; + } + + private String getValidationErrorMessage(FieldError fieldError, Locale currentLocale) { + String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale); + + //If the message was not found, return the most accurate field error code instead. + //You can remove this check if you prefer to get the default error message. + if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) { + String[] fieldErrorCodes = fieldError.getCodes(); + localizedErrorMessage = fieldErrorCodes[0]; + } + + return localizedErrorMessage; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java new file mode 100644 index 0000000..8355c7b --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTO.java @@ -0,0 +1,36 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import org.springframework.http.HttpStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class contains the information of validation errors that are found + * from a controller method parameter that is annotated with the + * {@link javax.validation.Valid} annotation. + * + * @author Petri Kainulainen + */ +final class ValidationErrorDTO { + + private final String code = HttpStatus.BAD_REQUEST.name(); + + private final List fieldErrors = new ArrayList<>(); + + ValidationErrorDTO() { + } + + void addFieldError(String field, String message) { + FieldErrorDTO error = new FieldErrorDTO(field, message); + fieldErrors.add(error); + } + + public String getCode() { + return code; + } + + public List getFieldErrors() { + return fieldErrors; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java new file mode 100644 index 0000000..141a948 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfHeaderFilter.java @@ -0,0 +1,46 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This filter reads the {@link org.springframework.security.web.csrf.CsrfToken} from the {@link HttpServletRequest} and + * sets its content to the {@link HttpServletResponse} headers. + * + * I borrowed this idea from this StackOverflow question. + * + * @author Petri Kainulainen + */ +public class CsrfHeaderFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(CsrfHeaderFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + LOGGER.trace("Reading CSRF token from the request."); + + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (token != null) { + LOGGER.trace("CSRF token was found. Creating HTTP response headers."); + response.setHeader("X-CSRF-HEADER", token.getHeaderName()); + response.setHeader("X-CSRF-PARAM", token.getParameterName()); + response.setHeader("X-CSRF-TOKEN", token.getToken()); + } + else { + LOGGER.trace("CSRF Token was not found. Doing nothing."); + } + + filterChain.doFilter(request, response); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java new file mode 100644 index 0000000..f6e70cb --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/CsrfTokenController.java @@ -0,0 +1,21 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Petri Kainulainen + */ +@RestController +public class CsrfTokenController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsrfTokenController.class); + + @RequestMapping(value = "/api/csrf", method = RequestMethod.HEAD) + public void getCsrfToken() { + LOGGER.info("Getting CSRF token."); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..887e25b --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication entry point returns the HTTP status code 401. + * @author Petri Kainulainen + */ +public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + LOGGER.info("Authentication required. Returning HTTP status code 401."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..daf635b --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication failure handler returns the HTTP status code 403. + * @author Petri Kainulainen + */ +public final class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationFailureHandler.class); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + LOGGER.info("Authentication failed with message: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failed."); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ff84785 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/RestAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * This authentication success handler returns the information of the authenticated + * user as JSON. + * + * @author Petri Kainulainen + */ +public final class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + LOGGER.info("Authentication was successful"); + response.sendRedirect(response.encodeRedirectURL("/api/authenticated-user")); + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java new file mode 100644 index 0000000..ef7959d --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserController.java @@ -0,0 +1,45 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller provides the public API that is used to return the information + * of the authenticated user. + * + * @author Petri Kainulainen + */ +@RestController +final class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + /** + * Returns the information of the authenticated user as JSON. The returned information + * contains the username and the user role of the authenticated user. + * + * @param authenticatedUser The information of the authenticated user. + * @return + */ + @RequestMapping(value = "/api/authenticated-user", method = RequestMethod.GET) + public UserDTO getAuthenticatedUser(@AuthenticationPrincipal User authenticatedUser) { + LOGGER.info("Getting authenticated user."); + + if (authenticatedUser == null) { + //If anonymous users can access this controller method, someone has changed + //the security configuration and it must be fixed. + LOGGER.error("Authenticated user is not found."); + throw new AccessDeniedException("Anonymous users cannot request the information of the authenticated user."); + } + else { + LOGGER.info("User with username: {} is authenticated", authenticatedUser.getUsername()); + return new UserDTO(authenticatedUser.getUsername(), authenticatedUser.getAuthorities()); + } + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java new file mode 100644 index 0000000..92b99ed --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserDTO.java @@ -0,0 +1,35 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import net.petrikainulainen.springdata.jpa.common.PreCondition; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * This class contains the information of the authenticated user. + * + * @author Petri Kainulainen + */ +public final class UserDTO { + + private final String username; + + private final UserRole role; + + UserDTO(String username, Collection authorities) { + PreCondition.isTrue(!username.isEmpty(), "Username cannot be empty."); + PreCondition.isTrue(authorities.size() == 1, "User must have only one granted authority."); + this.username = username; + + GrantedAuthority authority = authorities.iterator().next(); + this.role = UserRole.valueOf(authority.getAuthority()); + } + + public String getUsername() { + return username; + } + + public UserRole getRole() { + return role; + } +} diff --git a/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java new file mode 100644 index 0000000..8b3e6a6 --- /dev/null +++ b/querydsl/src/main/java/net/petrikainulainen/springdata/jpa/web/security/UserRole.java @@ -0,0 +1,8 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +/** + * @author Petri Kainulainen + */ +enum UserRole { + ROLE_USER +} diff --git a/querydsl/src/main/resources/application.properties b/querydsl/src/main/resources/application.properties new file mode 100644 index 0000000..7ac8298 --- /dev/null +++ b/querydsl/src/main/resources/application.properties @@ -0,0 +1,12 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false \ No newline at end of file diff --git a/querydsl/src/main/resources/applicationContext-persistence.xml b/querydsl/src/main/resources/applicationContext-persistence.xml new file mode 100644 index 0000000..e9149e9 --- /dev/null +++ b/querydsl/src/main/resources/applicationContext-persistence.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${hibernate.dialect} + + + ${hibernate.hbm2ddl.auto} + + + ${hibernate.ejb.naming_strategy} + + + ${hibernate.show_sql} + + + ${hibernate.format_sql} + + + + + + + + + + + + + \ No newline at end of file diff --git a/querydsl/src/main/resources/applicationContext-web.xml b/querydsl/src/main/resources/applicationContext-web.xml new file mode 100644 index 0000000..db48af6 --- /dev/null +++ b/querydsl/src/main/resources/applicationContext-web.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + WRITE_DATES_AS_TIMESTAMPS + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/querydsl/src/main/resources/applicationContext.xml b/querydsl/src/main/resources/applicationContext.xml new file mode 100644 index 0000000..b9ee424 --- /dev/null +++ b/querydsl/src/main/resources/applicationContext.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/querydsl/src/main/resources/i18n/messages.properties b/querydsl/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..0e376f5 --- /dev/null +++ b/querydsl/src/main/resources/i18n/messages.properties @@ -0,0 +1,5 @@ +error.todo.entry.not.found=No todo entry was found by using id: {0} + +NotEmpty.todoDTO.title=The title cannot be empty +Size.todoDTO.description=The maximum length of description is 500 characters +Size.todoDTO.title=The maximum length of title is 100 characters \ No newline at end of file diff --git a/querydsl/src/main/resources/integration-test.properties b/querydsl/src/main/resources/integration-test.properties new file mode 100644 index 0000000..3605c55 --- /dev/null +++ b/querydsl/src/main/resources/integration-test.properties @@ -0,0 +1,14 @@ +#Database Configuration +db.driver=org.h2.Driver +db.url=jdbc:h2:mem:datajpa;DB_CLOSE_ON_EXIT=FALSE +db.username=sa +db.password= + +#Hibernate Configuration +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.format_sql=true +hibernate.hbm2ddl.auto=create-drop +hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy +hibernate.show_sql=false + +test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1 \ No newline at end of file diff --git a/tutorial-part-one/src/main/resources/log4j.properties b/querydsl/src/main/resources/log4j.properties similarity index 75% rename from tutorial-part-one/src/main/resources/log4j.properties rename to querydsl/src/main/resources/log4j.properties index 5ad34eb..668d97a 100644 --- a/tutorial-part-one/src/main/resources/log4j.properties +++ b/querydsl/src/main/resources/log4j.properties @@ -3,4 +3,6 @@ log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG + +log4j.logger.org.hibernate=INFO +log4j.logger.org.springframework=INFO \ No newline at end of file diff --git a/querydsl/src/main/webapp/WEB-INF/jsp/frontend/client.jsp b/querydsl/src/main/webapp/WEB-INF/jsp/frontend/client.jsp new file mode 100644 index 0000000..84158d0 --- /dev/null +++ b/querydsl/src/main/webapp/WEB-INF/jsp/frontend/client.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ +
+
+

+

+
+
+ + + diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java new file mode 100644 index 0000000..c51749a --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/PageBuilder.java @@ -0,0 +1,39 @@ +package net.petrikainulainen.springdata.jpa; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Petri Kainulainen + */ +public class PageBuilder { + + private List elements = new ArrayList<>(); + private Pageable pageRequest; + private int totalElements; + + public PageBuilder() {} + + public PageBuilder elements(List elements) { + this.elements = elements; + return this; + } + + public PageBuilder pageRequest(Pageable pageRequest) { + this.pageRequest = pageRequest; + return this; + } + + public PageBuilder totalElements(int totalElements) { + this.totalElements = totalElements; + return this; + } + + public Page build() { + return new PageImpl(elements, pageRequest, totalElements); + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java new file mode 100644 index 0000000..7e90183 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/common/PreConditionTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.common; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Petri Kainulainen + */ +public class PreConditionTest { + + private static final String STATIC_ERROR_MESSAGE = "static error message"; + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, "Dynamic error message with parameter: %d", 1L); + } + + @Test + public void isTrueWithDynamicErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, "Dynamic error message with parameter: %d", 1L)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic error message with parameter: 1"); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsTrue_ShouldNotThrowException() { + PreCondition.isTrue(true, STATIC_ERROR_MESSAGE); + } + + @Test + public void isTrueWithStaticErrorMessage_ExpressionIsFalse_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.isTrue(false, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsNotEmpty_ShouldNotThrowException() { + PreCondition.notEmpty(" ", STATIC_ERROR_MESSAGE); + } + + @Test + public void notEmpty_StringIsEmpty_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notEmpty("", STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNotNull_ShouldNotThrowException() { + PreCondition.notNull(new Object(), STATIC_ERROR_MESSAGE); + } + + @Test + public void notNull_ObjectIsNull_ShouldThrowException() { + assertThatThrownBy(() -> PreCondition.notNull(null, STATIC_ERROR_MESSAGE)) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage(STATIC_ERROR_MESSAGE); + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java new file mode 100644 index 0000000..943da93 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoSearchServiceTest.java @@ -0,0 +1,164 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.mysema.query.types.Predicate; +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.Arrays; + +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoSearchServiceTest { + + private static final String SEARCH_TERM = "itl"; + + private TodoRepository repository; + private RepositoryTodoSearchService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoSearchService(repository); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final int PAGE_SIZE = 5; + private final String SORT_PROPERTY = "title"; + + private Pageable pageRequest; + + @Before + public void createPageRequest() { + Sort sort = new Sort(Sort.Direction.ASC, SORT_PROPERTY); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findAll(isA(Predicate.class), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageWithRequestedPageNumber() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumber()).isEqualTo(PAGE_NUMBER); + } + + @Test + public void shouldReturnPageWithRequestedPageSize() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSize()).isEqualTo(PAGE_SIZE); + } + + @Test + public void shouldReturnPageThatIsSortedInAscendingOrderByUsingSortProperty() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getSort().getOrderFor(SORT_PROPERTY).getDirection()) + .isEqualTo(Sort.Direction.ASC); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnZeroTodoEntries() { + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(repository.findAll(isA(Predicate.class), eq(pageRequest))).willReturn(emptyPage); + } + + @Test + public void shouldReturnEmptyPage() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage).isEmpty(); + } + + @Test + public void shouldReturnPageWithTotalElementCountZero() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(0); + } + } + + public class WhenOneTodoEntryIsFound { + + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final Long ID = 20L; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Page resultPage = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + + given(repository.findAll(isA(Predicate.class), eq(pageRequest))).willReturn(resultPage); + } + + @Test + public void shouldReturnPageThatHasOneTodoEntry() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getNumberOfElements()).isEqualTo(1); + } + + @Test + public void shouldReturnPageThatHasCorrectInformation() { + TodoDTO found = service.findBySearchTerm(SEARCH_TERM, pageRequest).getContent().get(0); + + assertThatTodoDTO(found) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnPageWithTotalElementCountOne() { + Page searchResultPage = service.findBySearchTerm(SEARCH_TERM, pageRequest); + assertThat(searchResultPage.getTotalElements()).isEqualTo(1); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java new file mode 100644 index 0000000..81bbdb5 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/RepositoryTodoServiceTest.java @@ -0,0 +1,387 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RepositoryTodoServiceTest { + + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + private static final Long ID = 20L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T22:29:05+02:00"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + private TodoRepository repository; + + private RepositoryTodoService service; + + @Before + public void setUp() { + repository = mock(TodoRepository.class); + service = new RepositoryTodoService(repository); + } + + public class Create { + + @Before + public void returnNewTodoEntry() { + given(repository.save(isA(Todo.class))).willAnswer( + invocationOnMock -> new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build() + ); + } + + @Test + public void shouldPersistNewTodoEntryWithCorrectInformation() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + service.create(newTodoEntry); + + verify(repository, times(1)).save( + assertArg(persisted -> assertThatTodoEntry(persisted) + .hasNoCreationAuditFieldValues() + .hasDescription(DESCRIPTION) + .hasNoId() + .hasNoModificationAuditFieldValues() + .hasTitle(TITLE) + ) + ); + verifyNoMoreInteractions(repository); + } + + @Test + public void shouldReturnTheInformationOfPersistedTodoEntry() { + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + + TodoDTO created = service.create(newTodoEntry); + assertThatTodoDTO(created) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.delete(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException ex = (TodoNotFoundException) thrown; + assertThat(ex.getId()).isEqualTo(ID); + } + + @Test + public void shouldNotDeleteTodoEntry() { + catchThrowable(() -> service.delete(ID)); + + verify(repository, never()).delete(isA(Todo.class)); + } + } + + public class WhenTodoEntryIsFound { + + private Todo deleted; + + @Before + public void returnDeletedTodoEntry() { + deleted = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(deleted)); + } + + @Test + public void shouldDeleteFoundTodoEntry() { + service.delete(ID); + + verify(repository, times(1)).delete(deleted); + } + + @Test + public void shouldReturnTheInformationOfDeletedTodoEntry() { + TodoDTO deleted = service.delete(ID); + + assertThatTodoDTO(deleted) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindAll { + + public class WhenNoTodoEntryAreFound { + + @Before + public void returnNoTodoEntries() { + given(repository.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyList() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).isEmpty(); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnOneTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntry() { + List todoEntries = service.findAll(); + + assertThat(todoEntries).hasSize(1); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO todoEntry = service.findAll().get(0); + + assertThatTodoDTO(todoEntry) + .hasId(ID) + .hasTitle(TITLE) + .hasDescription(DESCRIPTION) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class FindOne { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + Throwable thrown = catchThrowable(() -> service.findById(ID)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + Todo found = new TodoBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(found)); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntry() { + TodoDTO returned = service.findById(ID); + + assertThatTodoDTO(returned) + .hasDescription(DESCRIPTION) + .hasId(ID) + .hasTitle(TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void returnNoTodoEntry() { + given(repository.findOne(ID)).willReturn(Optional.empty()); + } + + @Test + public void shouldThrowExceptionWithCorrectId() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + Throwable thrown = catchThrowable(() -> service.update(updatedTodoEntry)); + + assertThat(thrown).isExactlyInstanceOf(TodoNotFoundException.class); + + TodoNotFoundException exception = (TodoNotFoundException) thrown; + assertThat(exception.getId()).isEqualTo(ID); + } + } + + public class WhenTodoEntryIsFound { + + private Todo updated; + + @Before + public void returnUpdatedTodoEntry() { + updated = new TodoBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(repository.findOne(ID)).willReturn(Optional.of(updated)); + } + + @Test + public void shouldUpdateTitleAndDescription() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldNotUpdateIdOrAuditInformation() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + service.update(updatedTodoEntry); + + assertThatTodoEntry(updated) + .hasId(ID) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntry() { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .description(UPDATED_DESCRIPTION) + .title(UPDATED_TITLE) + .build(); + + TodoDTO returnedTodoEntry = service.update(updatedTodoEntry); + + assertThatTodoDTO(returnedTodoEntry) + .hasDescription(UPDATED_DESCRIPTION) + .hasId(ID) + .hasTitle(UPDATED_TITLE) + .wasCreatedAt(CREATION_TIME) + .wasCreatedByUser(CREATED_BY_USER) + .wasModifiedAt(MODIFICATION_TIME) + .wasModifiedByUser(MODIFIED_BY_USER); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java new file mode 100644 index 0000000..ed98667 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TestUtil.java @@ -0,0 +1,26 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author Petri Kainulainen + */ +public final class TestUtil { + + private TestUtil() {} + + public static String createStringWithLength(int length) { + StringBuilder string = new StringBuilder(); + + for (int index = 0; index < length; index++) { + string.append("a"); + } + + return string.toString(); + } + + public static ZonedDateTime parseDateTime(String dateAndTime) { + return ZonedDateTime.parse(dateAndTime, DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java new file mode 100644 index 0000000..e88f27c --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoAssert.java @@ -0,0 +1,198 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This class provides a fluent API that can be used for writing assertions + * to {@link net.petrikainulainen.springdata.jpa.todo.Todo} objects. + * + * @author Petri Kainulainen + */ +final class TodoAssert extends AbstractAssert { + + private TodoAssert(Todo actual) { + super(actual, TodoAssert.class); + } + + static TodoAssert assertThatTodoEntry(Todo actual) { + return new TodoAssert(actual); + } + + TodoAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage(String.format( + "Expected description to be <%s> but was <%s>.", + expectedDescription, + actualDescription + )) + .isEqualTo(expectedDescription); + + return this; + } + + TodoAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creationTime to be but was <%s>", + actualCreationTime + ) + .isNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + return this; + } + + TodoAssert hasNoDescription() { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage("Expected description to be but was <%s>", actualDescription) + .isNull(); + + return this; + } + + TodoAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be <%d> but was <%d>", + expectedId, + actualId + ) + .isEqualTo(expectedId); + + return this; + } + + TodoAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>.", actualId) + .isNull(); + + return this; + } + + TodoAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>.", + actualModificationTime + ) + .isNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modificationTime to be but was <%s>", + actualModificationTime + ) + .isNull(); + + return this; + } + + TodoAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>.", + expectedTitle, + actualTitle + ) + .isEqualTo(actualTitle); + + return this; + } + + public TodoAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java new file mode 100644 index 0000000..90ee955 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoBuilder.java @@ -0,0 +1,71 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +class TodoBuilder { + + private Long id; + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + TodoBuilder() {} + + TodoBuilder id(Long id) { + this.id = id; + return this; + } + + TodoBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + TodoBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + TodoBuilder description(String description) { + this.description = description; + return this; + } + + TodoBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + TodoBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + TodoBuilder title(String title) { + this.title = title; + return this; + } + + Todo build() { + Todo build = Todo.getBuilder() + .title(title) + .description(description) + .build(); + + ReflectionTestUtils.setField(build, "createdByUser", createdByUser); + ReflectionTestUtils.setField(build, "creationTime", creationTime); + ReflectionTestUtils.setField(build, "id", id); + ReflectionTestUtils.setField(build, "modifiedByUser", modifiedByUser); + ReflectionTestUtils.setField(build, "modificationTime", modificationTime); + + return build; + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java new file mode 100644 index 0000000..462d90d --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOAssert.java @@ -0,0 +1,179 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import org.assertj.core.api.AbstractAssert; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +public final class TodoDTOAssert extends AbstractAssert { + + private TodoDTOAssert(TodoDTO actual) { + super(actual, TodoDTOAssert.class); + } + + public static TodoDTOAssert assertThatTodoDTO(TodoDTO actual) { + return new TodoDTOAssert(actual); + } + + public TodoDTOAssert hasDescription(String expectedDescription) { + isNotNull(); + + String actualDescription = actual.getDescription(); + assertThat(actualDescription) + .overridingErrorMessage( + "Expected description to be <%s> but was <%s>", + expectedDescription, + actualDescription + ) + .isEqualTo(expectedDescription); + + return this; + } + + public TodoDTOAssert hasId(Long expectedId) { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage( + "Expected id to be <%d> but was <%d>", + actualId, + expectedId + ) + .isEqualTo(expectedId); + + return this; + } + + public TodoDTOAssert hasNoCreationAuditFieldValues() { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be but was <%s>", + actualCreatedByUser + ) + .isNull(); + + ZonedDateTime actualCreationTime = actual.getCreationTime(); + assertThat(actualCreationTime) + .overridingErrorMessage("Expected creationTime to be but was <%s>", actualCreationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoId() { + isNotNull(); + + Long actualId = actual.getId(); + assertThat(actualId) + .overridingErrorMessage("Expected id to be but was <%d>", actualId) + .isNull(); + + return this; + } + + public TodoDTOAssert hasNoModificationAuditFieldValues() { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be but was <%s>", + actualModifiedByUser + ) + .isNull(); + + ZonedDateTime actualModificationTime = actual.getModificationTime(); + assertThat(actualModificationTime) + .overridingErrorMessage("Expected modification time to be but was <%d>", actualModificationTime) + .isNull(); + + return this; + } + + public TodoDTOAssert hasTitle(String expectedTitle) { + isNotNull(); + + String actualTitle = actual.getTitle(); + assertThat(actualTitle) + .overridingErrorMessage( + "Expected title to be <%s> but was <%s>", + expectedTitle, + actualTitle + ) + .isEqualTo(expectedTitle); + + return this; + } + + public TodoDTOAssert wasCreatedAt(String creationTime) { + isNotNull(); + + ZonedDateTime expectedCreationTime = TestUtil.parseDateTime(creationTime); + ZonedDateTime actualCreationTime = actual.getCreationTime(); + + assertThat(actualCreationTime) + .overridingErrorMessage( + "Expected creation time to be <%s> but was <%s>", + expectedCreationTime, + actualCreationTime + ) + .isEqualTo(expectedCreationTime); + + return this; + } + + public TodoDTOAssert wasCreatedByUser(String expectedCreatedByUser) { + isNotNull(); + + String actualCreatedByUser = actual.getCreatedByUser(); + assertThat(actualCreatedByUser) + .overridingErrorMessage( + "Expected createdByUser to be <%s> but was <%s>", + expectedCreatedByUser, + actualCreatedByUser + ) + .isEqualTo(expectedCreatedByUser); + + return this; + } + + public TodoDTOAssert wasModifiedAt(String modificationTime) { + isNotNull(); + + ZonedDateTime expectedModificationTime = TestUtil.parseDateTime(modificationTime); + ZonedDateTime actualModificationTime = actual.getModificationTime(); + + assertThat(actualModificationTime) + .overridingErrorMessage( + "Expected modification time to be <%s> but was <%s>", + expectedModificationTime, + actualModificationTime + ) + .isEqualTo(actualModificationTime); + + return this; + } + + public TodoDTOAssert wasModifiedByUser(String expectedModifiedByUser) { + isNotNull(); + + String actualModifiedByUser = actual.getModifiedByUser(); + assertThat(actualModifiedByUser) + .overridingErrorMessage( + "Expected modifiedByUser to be <%s> but was <%s>", + expectedModifiedByUser, + actualModifiedByUser + ) + .isEqualTo(expectedModifiedByUser); + + return this; + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java new file mode 100644 index 0000000..e0b5505 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoDTOBuilder.java @@ -0,0 +1,68 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import java.time.ZonedDateTime; + +/** + * @author Petri Kainulainen + */ +public class TodoDTOBuilder { + + private String createdByUser; + private ZonedDateTime creationTime; + private String description; + private Long id; + private String modifiedByUser; + private ZonedDateTime modificationTime; + private String title = "NOT_IMPORTANT"; + + public TodoDTOBuilder() {} + + public TodoDTOBuilder createdByUser(String createdByUser) { + this.createdByUser = createdByUser; + return this; + } + + public TodoDTOBuilder creationTime(String creationTime) { + this.creationTime = TestUtil.parseDateTime(creationTime); + return this; + } + + public TodoDTOBuilder description(String description) { + this.description = description; + return this; + } + + public TodoDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public TodoDTOBuilder modifiedByUser(String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + return this; + } + + public TodoDTOBuilder modificationTime(String modificationTime) { + this.modificationTime = TestUtil.parseDateTime(modificationTime); + return this; + } + + public TodoDTOBuilder title(String title) { + this.title = title; + return this; + } + + public TodoDTO build() { + TodoDTO build = new TodoDTO(); + + build.setCreatedByUser(createdByUser); + build.setCreationTime(creationTime); + build.setDescription(description); + build.setId(id); + build.setModifiedByUser(modifiedByUser); + build.setModificationTime(modificationTime); + build.setTitle(title); + + return build; + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java new file mode 100644 index 0000000..c5ff69d --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/todo/TodoTest.java @@ -0,0 +1,340 @@ +package net.petrikainulainen.springdata.jpa.todo; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static net.petrikainulainen.springdata.jpa.todo.TodoAssert.assertThatTodoEntry; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoTest { + + private static final int MAX_LENGTH_DESCRIPTION = 500; + private static final int MAX_LENGTH_TITLE = 100; + + private static final String DESCRIPTION = "description"; + private static final String TITLE = "title"; + + private static final String UPDATED_DESCRIPTION = "updatedDescription"; + private static final String UPDATED_TITLE = "updatedTitle"; + + public class Build { + + public class WhenTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(null) + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title("") + .description(DESCRIPTION) + .build(); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(tooLongTitle) + .description(DESCRIPTION) + .build(); + } + } + } + + public class WhenDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Todo.getBuilder() + .title(TITLE) + .description(tooLongDescription) + .build(); + } + } + + public class WhenTitleAndDescriptionAreValid { + + @Test + public void shouldNotSetId() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoId(); + } + + @Test + public void shouldNotSetCreationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoCreationAuditFieldValues(); + } + + @Test + public void shouldNotSetModificationAuditFieldValues() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasNoModificationAuditFieldValues(); + } + + @Test + public void shouldSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasDescription(DESCRIPTION); + } + + @Test + public void shouldSetTitle() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(TITLE); + } + + public class WhenMaxLengthTitleIsGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldCreateNewObjectAndSetTitle() { + Todo build = Todo.getBuilder() + .title(maxLengthTitle) + .description(DESCRIPTION) + .build(); + + assertThatTodoEntry(build) + .hasTitle(maxLengthTitle); + } + } + + public class WhenMaxLengthDescriptionIsGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + + } + + @Test + public void shouldCreateNewObjectAndSetDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .description(maxLengthDescription) + .build(); + + assertThatTodoEntry(build) + .hasDescription(maxLengthDescription); + } + } + + public class WhenNoDescriptionIsGiven { + + @Test + public void shouldCreateNewObjectWithoutDescription() { + Todo build = Todo.getBuilder() + .title(TITLE) + .build(); + + assertThatTodoEntry(build) + .hasNoDescription(); + } + } + } + } + + public class Update { + + private Todo updated; + + @Before + public void createUpdatedTodoEntry() { + updated = Todo.getBuilder() + .description(DESCRIPTION) + .title(TITLE) + .build(); + } + + public class WhenNewTitleIsInvalid { + + public class WhenTitleIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + updated.update(null, UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update("", UPDATED_DESCRIPTION); + } + } + + public class WhenTitleIsTooLong { + + private String tooLongTitle; + + @Before + public void createTooLongTitle() { + tooLongTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE + 1); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(tooLongTitle, UPDATED_DESCRIPTION); + } + } + } + + public class WhenNewDescriptionIsTooLong { + + private String tooLongDescription; + + @Before + public void createTooLongDescription() { + tooLongDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION + 1); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + updated.update(UPDATED_TITLE, tooLongDescription); + } + } + + public class WhenNewTitleAndNewDescriptionAreValid { + + public class WhenMaxLengthTitleAndNewDescriptionAreGiven { + + private String maxLengthTitle; + + @Before + public void createMaxLengthTitle() { + maxLengthTitle = TestUtil.createStringWithLength(MAX_LENGTH_TITLE); + } + + @Test + public void shouldUpdateTitle() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasTitle(maxLengthTitle); + } + + @Test + public void shouldUpdateDescription() { + updated.update(maxLengthTitle, UPDATED_DESCRIPTION); + + assertThatTodoEntry(updated) + .hasDescription(UPDATED_DESCRIPTION); + } + } + + public class WhenNewTitleIsGivenAndNewDescriptionIsNull { + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldRemoveDescription() { + updated.update(UPDATED_TITLE, null); + + assertThatTodoEntry(updated) + .hasNoDescription(); + } + } + + public class WhenNewTitleAndMaxLengthDescriptionAreGiven { + + private String maxLengthDescription; + + @Before + public void createMaxLengthDescription() { + maxLengthDescription = TestUtil.createStringWithLength(MAX_LENGTH_DESCRIPTION); + } + + @Test + public void shouldUpdateTitle() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasTitle(UPDATED_TITLE); + } + + @Test + public void shouldUpdateDescription() { + updated.update(UPDATED_TITLE, maxLengthDescription); + + assertThatTodoEntry(updated) + .hasDescription(maxLengthDescription); + } + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java new file mode 100644 index 0000000..a0dd3eb --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoControllerTest.java @@ -0,0 +1,691 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TestUtil; +import net.petrikainulainen.springdata.jpa.todo.TodoCrudService; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static net.petrikainulainen.springdata.jpa.todo.TodoDTOAssert.assertThatTodoDTO; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoControllerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + private static final String CREATED_BY_USER = "createdByUser"; + private static final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private static final String DESCRIPTION = "description"; + + private static final String ERROR_MESSAGE_KEY_MISSING_TITLE = "NotEmpty.todoDTO.title"; + private static final String ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION = "Size.todoDTO.description"; + private static final String ERROR_MESSAGE_KEY_TOO_LONG_TITLE = "Size.todoDTO.title"; + + private static final Long ID = 1L; + private static final String MODIFIED_BY_USER = "modifiedByUser"; + private static final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private static final String TITLE = "title"; + + private MockMvc mockMvc; + + private TodoCrudService crudService; + + private StaticMessageSource messageSource; + + @Before + public void setUp() { + crudService = mock(TodoCrudService.class); + + messageSource = new StaticMessageSource(); + messageSource.setUseCodeAsDefaultMessage(true); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(crudService)) + .setHandlerExceptionResolvers(WebTestConfig.restErrorHandler(messageSource)) + .setLocaleResolver(WebTestConfig.fixedLocaleResolver(CURRENT_LOCALE)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setValidator(WebTestConfig.validator()) + .build(); + } + + public class Create { + + public class WhenTodoEntryIsNotValid { + + public class WhenTodoEntryIsEmpty { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + TodoDTO emptyTodoEntry = new TodoDTO(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(emptyTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotCreateNewTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO newTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .title(tooLongTitle) + .build(); + + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + private TodoDTO newTodoEntry; + + @Before + public void createInputAndReturnNewTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + newTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .title(maxLengthTitle) + .build(); + + TodoDTO created = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.create(isA(TodoDTO.class))).willReturn(created); + } + + @Test + public void shouldReturnResponseStatusCreated() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(status().isCreated()); + } + + @Test + public void shouldReturnCreatedTodoEntryAsJson() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldCreateNewTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(post("/api/todo") + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(newTodoEntry)) + ); + + verify(crudService, times(1)).create( + assertArg(created -> assertThatTodoDTO(created) + .hasDescription(maxLengthDescription) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoId() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + + public class Delete { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwNotFoundException() { + given(crudService.delete(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnDeletedTodoEntry() { + TodoDTO deleted = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.delete(ID)).willReturn(deleted); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfDeletedTodoEntryAsJson() throws Exception { + mockMvc.perform(delete("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class FindAll { + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(status().isOk()); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnNoTodoEntries() { + given(crudService.findAll()).willReturn(new ArrayList<>()); + } + + @Test + public void shouldReturnEmptyListAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + } + + public class WhenOneTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findAll()).willReturn(Arrays.asList(found)); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo")) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$[0].title", is(TITLE))); + } + } + } + + public class FindById { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.findById(ID)).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + @Before + public void returnFoundTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + given(crudService.findById(ID)).willReturn(found); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfFoundTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/{id}", ID)) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(DESCRIPTION))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(TITLE))); + } + } + } + + public class Update { + + public class WhenTodoEntryIsNotFound { + + @Before + public void throwTodoNotFoundException() { + given(crudService.update(isA(TodoDTO.class))).willThrow(new TodoNotFoundException(ID)); + } + + @Test + public void shouldReturnResponseStatusNotFound() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldReturnErrorMessageAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .id(ID) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_TODO_ENTRY_NOT_FOUND))) + .andExpect(jsonPath("message", is(ERROR_MESSAGE_KEY_TODO_ENTRY_NOT_FOUND))); + } + } + + public class WhenTodoEntryIsFound { + + public class WhenTodoEntryIsNotValid { + + public class WhenTitleAndDescriptionAreMissing { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorAboutMissingTitleAsJson() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(1))) + .andExpect(jsonPath("$.fieldErrors[0].field", is(WebTestConstants.FIELD_NAME_TITLE))) + .andExpect(jsonPath("$.fieldErrors[0].message", is(ERROR_MESSAGE_KEY_MISSING_TITLE))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(null) + .id(ID) + .title(null) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + + public class WhenTitleAndDescriptionAreTooLong { + + @Test + public void shouldReturnResponseStatusBadRequest() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnValidationErrorsAboutTitleAndDescriptionAsJson() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.code", is(WebTestConstants.ERROR_CODE_VALIDATION_FAILED))) + .andExpect(jsonPath("$.fieldErrors", hasSize(2))) + .andExpect(jsonPath("$.fieldErrors[*].field", containsInAnyOrder( + WebTestConstants.FIELD_NAME_DESCRIPTION, + WebTestConstants.FIELD_NAME_TITLE + ))) + .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder( + ERROR_MESSAGE_KEY_TOO_LONG_DESCRIPTION, + ERROR_MESSAGE_KEY_TOO_LONG_TITLE + ))); + } + + @Test + public void shouldNotUpdateTodoEntry() throws Exception { + String tooLongDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION + 1); + String tooLongTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE + 1); + + TodoDTO updatedTodoEntry = new TodoDTOBuilder() + .description(tooLongDescription) + .id(ID) + .title(tooLongTitle) + .build(); + + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verifyZeroInteractions(crudService); + } + } + } + + public class WhenTodoEntryIsValid { + + public class WhenMaxLengthTitleAndDescriptionAreGiven { + + private String maxLengthDescription; + private String maxLengthTitle; + + TodoDTO updatedTodoEntry; + + @Before + public void createInputAndReturnUpdatedTodoEntry() { + maxLengthDescription = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_DESCRIPTION); + maxLengthTitle = TestUtil.createStringWithLength(WebTestConstants.MAX_LENGTH_TITLE); + + updatedTodoEntry = new TodoDTOBuilder() + .description(maxLengthDescription) + .id(ID) + .title(maxLengthTitle) + .build(); + + TodoDTO updated = new TodoDTOBuilder() + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(maxLengthDescription) + .id(ID) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(maxLengthTitle) + .build(); + given(crudService.update(isA(TodoDTO.class))).willReturn(updated); + } + + @Test + public void shouldReturnResponseStatusOk() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnInformationOfUpdatedTodoEntryAsJson() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.description", is(maxLengthDescription))) + .andExpect(jsonPath("$.id", is(ID.intValue()))) + .andExpect(jsonPath("$.modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.title", is(maxLengthTitle))); + } + + @Test + public void shouldUpdateTodoEntryWithCorrectInformation() throws Exception { + mockMvc.perform(put("/api/todo/{id}", ID) + .contentType(WebTestConstants.APPLICATION_JSON_UTF8) + .content(WebTestUtil.convertObjectToJsonBytes(updatedTodoEntry)) + ); + + verify(crudService, times(1)).update( + assertArg(updated -> assertThatTodoDTO(updated) + .hasDescription(maxLengthDescription) + .hasId(ID) + .hasTitle(maxLengthTitle) + .hasNoCreationAuditFieldValues() + .hasNoModificationAuditFieldValues() + ) + ); + } + } + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java new file mode 100644 index 0000000..98c060d --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/TodoSearchControllerTest.java @@ -0,0 +1,250 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.PageBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoDTOBuilder; +import net.petrikainulainen.springdata.jpa.todo.TodoSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class TodoSearchControllerTest { + + private MockMvc mockMvc; + + private TodoSearchService searchService; + + @Before + public void setUp() { + searchService = mock(TodoSearchService.class); + + mockMvc = MockMvcBuilders.standaloneSetup(new TodoSearchController(searchService)) + .setMessageConverters(WebTestConfig.jacksonDateTimeConverter()) + .setCustomArgumentResolvers(WebTestConfig.pageRequestArgumentResolver()) + .build(); + } + + public class FindBySearchTerm { + + private final int PAGE_NUMBER = 1; + private final String PAGE_NUMBER_STRING = "1"; + private final int PAGE_SIZE = 5; + private final String PAGE_SIZE_STRING = "5"; + private final String SEARCH_TERM = "itl"; + + private Pageable pageRequest; + + @Before + public void setUp() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnHttpResponseStatusOk() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnPageNumberAndPageSizeAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.number", is(PAGE_NUMBER))) + .andExpect(jsonPath("$.size", is(PAGE_SIZE))); + } + + @Test + public void shouldReturnSortInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.sort[*].direction[0]", is(WebTestConstants.SORT_DIRECTION_ASC))) + .andExpect(jsonPath("$.sort[*].property[0]", is(WebTestConstants.FIELD_NAME_TITLE))); + } + + @Test + public void shouldReturnPTotalElementInformationAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(jsonPath("$.totalElements", is(0))); + } + + @Test + public void shouldPassSearchTermForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class)); + } + + @Test + public void shouldPassPageSizeAndNumberForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> { + assertThat(pageRequest.getPageNumber()).isEqualTo(PAGE_NUMBER); + assertThat(pageRequest.getPageSize()).isEqualTo(PAGE_SIZE); + } + )); + } + + @Test + public void shouldPassSortForwardToSearchService() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_PAGE_NUMBER, PAGE_NUMBER_STRING) + .param(WebTestConstants.REQUEST_PARAM_PAGE_SIZE, PAGE_SIZE_STRING) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ); + + verify(searchService, times(1)).findBySearchTerm(isA(String.class), assertArg( + pageRequest -> assertThat( + pageRequest.getSort().getOrderFor(WebTestConstants.FIELD_NAME_TITLE).getDirection()) + .isEqualTo(Sort.Direction.ASC) + ) + ); + } + + public class WhenNoTodoEntriesAreFound { + + @Before + public void returnEmptyPage() { + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page emptyPage = new PageBuilder() + .elements(new ArrayList<>()) + .pageRequest(pageRequest) + .totalElements(0) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(emptyPage); + } + + @Test + public void shouldReturnPageAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements", is(0))); + } + } + + + public class WhenOneTodoEntryIsFound { + + private final Long ID= 1L; + private final String CREATED_BY_USER = "createdByUser"; + private final String CREATION_TIME = "2014-12-24T22:28:39+02:00"; + private final String DESCRIPTION = "description"; + private final String MODIFIED_BY_USER = "modifiedByUser"; + private final String MODIFICATION_TIME = "2014-12-24T14:28:39+02:00"; + private final String TITLE = "title"; + + @Before + public void returnOneTodoEntry() { + TodoDTO found = new TodoDTOBuilder() + .id(ID) + .createdByUser(CREATED_BY_USER) + .creationTime(CREATION_TIME) + .description(DESCRIPTION) + .modifiedByUser(MODIFIED_BY_USER) + .modificationTime(MODIFICATION_TIME) + .title(TITLE) + .build(); + + Sort sort = new Sort(Sort.Direction.ASC, WebTestConstants.FIELD_NAME_TITLE); + pageRequest = new PageRequest(PAGE_NUMBER, PAGE_SIZE, sort); + + Page pageWithOneTodoEntry = new PageBuilder() + .elements(Arrays.asList(found)) + .pageRequest(pageRequest) + .totalElements(1) + .build(); + given(searchService.findBySearchTerm(eq(SEARCH_TERM), isA(Pageable.class))).willReturn(pageWithOneTodoEntry); + } + + @Test + public void shouldReturnOneTodoEntryAsJson() throws Exception { + mockMvc.perform(get("/api/todo/search") + .param(WebTestConstants.REQUEST_PARAM_SEARCH_TERM, SEARCH_TERM) + .param(WebTestConstants.REQUEST_PARAM_SORT, WebTestConstants.FIELD_NAME_TITLE) + ) + .andExpect(content().contentType(WebTestConstants.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].id", is(ID.intValue()))) + .andExpect(jsonPath("$.content[0].createdByUser", is(CREATED_BY_USER))) + .andExpect(jsonPath("$.content[0].creationTime", is(CREATION_TIME))) + .andExpect(jsonPath("$.content[0].description", is(DESCRIPTION))) + .andExpect(jsonPath("$.content[0].modifiedByUser", is(MODIFIED_BY_USER))) + .andExpect(jsonPath("$.content[0].modificationTime", is(MODIFICATION_TIME))) + .andExpect(jsonPath("$.content[0].title", is(TITLE))); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java new file mode 100644 index 0000000..8578c38 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConfig.java @@ -0,0 +1,122 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import net.petrikainulainen.springdata.jpa.web.error.RestErrorHandler; +import org.springframework.context.MessageSource; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.FixedLocaleResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Locale; + +/** + * This factory class provides methods that can be used to create objects that are useful + * when we are writing unit tests for our controller methods by using the Spring MVC Test + * framework. + * + * @author Petri Kainulainen + */ +final class WebTestConfig { + + private WebTestConfig() {} + + /** + * Configures a {@link org.springframework.web.servlet.LocaleResolver} that always returns the + * configured {@link java.util.Locale}. + * + * @return + */ + static LocaleResolver fixedLocaleResolver(Locale fixedLocale) { + return new FixedLocaleResolver(fixedLocale); + } + + /** + * This method creates a custom {@link org.springframework.http.converter.HttpMessageConverter} which ensures that: + * + *
    + *
  • Null values are ignored.
  • + *
  • + * The new Java 8 date objects are serialized in standard + * ISO-8601 string representation. + *
  • + *
+ * + * @return + */ + static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() { + ObjectMapper objectMapper = new ObjectMapper(); + + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JSR310Module()); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return converter; + } + + /** + * This method ensures that the {@link RestErrorHandler} class + * is used to handle the exceptions thrown by the tested controller. I borrowed this idea from + * this StackOverflow answer. + * + * @return an error handler component that delegates relevant exceptions forward to the {@link RestErrorHandler} class. + */ + static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) { + final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() { + @Override + protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerMethod handlerMethod, + final Exception exception) { + Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception); + if (method != null) { + return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method); + } + return super.getExceptionHandlerMethod(handlerMethod, exception); + } + }; + exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter())); + exceptionResolver.afterPropertiesSet(); + return exceptionResolver; + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Sort} objects by using the request params of the + * incoming request. + * @return + */ + static SortHandlerMethodArgumentResolver sortArgumentResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + /** + * This method returns a {@link org.springframework.web.method.support.HandlerMethodArgumentResolver} that can + * construct {@link org.springframework.data.domain.Pageable} objects by using the request params of the + * incoming request. + * @return + */ + static PageableHandlerMethodArgumentResolver pageRequestArgumentResolver() { + return new PageableHandlerMethodArgumentResolver(sortArgumentResolver()); + } + + /** + * This method creates a validator object that adds support for bean validation API 1.0 and 1.1. + * + * @return The created validator object. + */ + static LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java new file mode 100644 index 0000000..c50f7df --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestConstants.java @@ -0,0 +1,38 @@ + +package net.petrikainulainen.springdata.jpa.web; + +import org.springframework.http.MediaType; + +import java.nio.charset.Charset; + +/** + * @author Petri Kainulainen + */ +public final class WebTestConstants { + + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), + MediaType.APPLICATION_JSON.getSubtype(), + Charset.forName("utf8") + ); + + static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + static final String ERROR_CODE_VALIDATION_FAILED = "BAD_REQUEST"; + + static final String FIELD_NAME_DESCRIPTION = "description"; + static final String FIELD_NAME_TITLE = "title"; + + static final int MAX_LENGTH_DESCRIPTION = 500; + static final int MAX_LENGTH_TITLE = 100; + + static final String REQUEST_PARAM_PAGE_NUMBER = "page"; + static final String REQUEST_PARAM_PAGE_SIZE = "size"; + static final String REQUEST_PARAM_SEARCH_TERM = "searchTerm"; + static final String REQUEST_PARAM_SORT = "sort"; + + static final String SORT_DIRECTION_ASC = "ASC"; + + /** + * Prevents instantiation. + */ + private WebTestConstants() {} +} \ No newline at end of file diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java new file mode 100644 index 0000000..9340fe6 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/WebTestUtil.java @@ -0,0 +1,29 @@ +package net.petrikainulainen.springdata.jpa.web; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +/** + * @author Petri Kainulainen + */ +final class WebTestUtil { + + /** + * Prevents instantiation + */ + private WebTestUtil() {} + + /** + * Transforms an object into JSON and returns the JSON as a byte array. + * @param object The object that is transformed into JSON. + * @return The JSON representation of an object as a byte array. + * @throws IOException + */ + static byte[] convertObjectToJsonBytes(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsBytes(object); + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java new file mode 100644 index 0000000..528a552 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ErrorDTOTest.java @@ -0,0 +1,61 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.ErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ErrorDTOTest { + + private static final String CODE = "code"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenCodeIsInvalid { + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenCodeIsNull() { + new ErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenCodeIsEmpty() { + new ErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new ErrorDTO(CODE, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new ErrorDTO(CODE, ""); + } + } + + public class WhenCodeAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetCode() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getCode()).isEqualTo(CODE); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + ErrorDTO error = new ErrorDTO(CODE, MESSAGE); + assertThat(error.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java new file mode 100644 index 0000000..25fc6bf --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/FieldErrorDTOTest.java @@ -0,0 +1,64 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class FieldErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class CreateNew { + + public class WhenFieldIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenFieldIsNull() { + new FieldErrorDTO(null, MESSAGE); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenFieldIsEmpty() { + new FieldErrorDTO("", MESSAGE); + } + } + + public class WhenMessageIsInvalid { + + @Test(expected = NullPointerException.class) + public void shouldThrowExceptionWhenMessageIsNull() { + new FieldErrorDTO(FIELD, null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenMessageIsEmpty() { + new FieldErrorDTO(FIELD, ""); + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewObjectAndSetField() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewObjectAndSetMessage() { + FieldErrorDTO fieldError = new FieldErrorDTO(FIELD, MESSAGE); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java new file mode 100644 index 0000000..5ff8f34 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/RestErrorHandlerTest.java @@ -0,0 +1,242 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.todo.TodoDTO; +import net.petrikainulainen.springdata.jpa.todo.TodoNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.MethodParameter; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.List; +import java.util.Locale; + +import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class RestErrorHandlerTest { + + private static final Locale CURRENT_LOCALE = Locale.US; + + private static final Long TODO_ID = 99L; + + private MessageSource messageSource; + + private RestErrorHandler errorHandler; + + @Before + public void setUp() { + messageSource = mock(MessageSource.class); + this.errorHandler = new RestErrorHandler(messageSource); + } + + public class HandleTodoEntryNotFound { + + private static final String ERROR_CODE_TODO_ENTRY_NOT_FOUND = "NOT_FOUND"; + + private static final String ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND = "error.todo.entry.not.found"; + private static final String ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND = "No todo entry was found by using id: 99"; + + @Before + public void returnErrorMessageNotFound() { + given(messageSource.getMessage( + isA(MessageSourceResolvable.class), + isA(Locale.class)) + ).willReturn(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldFindErrorMessageByUsingCurrentLocale() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage(isA(MessageSourceResolvable.class), eq(CURRENT_LOCALE)); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectId() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getArguments()) + .containsOnly(TODO_ID) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldFindErrorMessageByUsingCorrectMessageCode() { + errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + verify(messageSource, times(1)).getMessage( + assertArg(messageRequest -> assertThat(messageRequest.getCodes()) + .containsOnly(ERROR_MESSAGE_CODE_TODO_ENTRY_NOT_FOUND) + ), + eq(CURRENT_LOCALE) + ); + } + + @Test + public void shouldReturnErrorThatHasCorrectErrorCode() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getCode()).isEqualTo(ERROR_CODE_TODO_ENTRY_NOT_FOUND); + } + + @Test + public void shouldReturnErrorThatHasCorrectMessage() { + ErrorDTO error = errorHandler.handleTodoEntryNotFound(new TodoNotFoundException(TODO_ID), CURRENT_LOCALE); + + assertThat(error.getMessage()).isEqualTo(ERROR_MESSAGE_TODO_ENTRY_NOT_FOUND); + } + } + + public class HandleValidationErrors { + + private static final String ERROR_CODE_VALIDATION_ERROR = "BAD_REQUEST"; + private static final String ERROR_MESSAGE_VALIDATION_ERROR = "validationError"; + + private static final String FIELD_DEFAULT_MESSAGE = "DefaultMessage"; + private static final String FIELD_WITH_VALIDATION_ERROR = "field"; + private static final String OBJECT_WITH_VALIDATION_ERROR = "todoDTO"; + + private static final String VALIDATION_ERROR_CODE_ACCURATE = "Error"; + private static final String VALIDATION_ERROR_CODE_LESS_ACCURATE = "Maybe"; + + public class WhenOneValidationErrorIsFound { + + public class WhenMessageIsFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(ERROR_MESSAGE_VALIDATION_ERROR); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasCorrectFieldErrorWithMessage() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(ERROR_MESSAGE_VALIDATION_ERROR); + } + } + + public class WhenMessageIsNotFound { + + private MethodArgumentNotValidException ex; + + @Before + public void createValidationErrorAndReturnDefaultErrorMessage() { + FieldError fieldError = new FieldErrorBuilder() + .defaultMessage(FIELD_DEFAULT_MESSAGE) + .errorCodes(VALIDATION_ERROR_CODE_ACCURATE, VALIDATION_ERROR_CODE_LESS_ACCURATE) + .fieldName(FIELD_WITH_VALIDATION_ERROR) + .build(); + given(messageSource.getMessage(fieldError, CURRENT_LOCALE)).willReturn(FIELD_DEFAULT_MESSAGE); + + ex = createExceptionWithFieldErrors(fieldError); + } + + @Test + public void shouldReturnErrorThatHasCorrectCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + assertThat(validationErrors.getCode()).isEqualTo(ERROR_CODE_VALIDATION_ERROR); + } + + @Test + public void shouldReturnErrorThatHasFieldErrorWithMostAccurateFieldErrorCode() { + ValidationErrorDTO validationErrors = errorHandler.handleValidationErrors(ex, CURRENT_LOCALE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO actualFieldError = fieldErrors.iterator().next(); + assertThat(actualFieldError.getField()).isEqualTo(FIELD_WITH_VALIDATION_ERROR); + assertThat(actualFieldError.getMessage()).isEqualTo(VALIDATION_ERROR_CODE_ACCURATE); + } + } + } + + private MethodArgumentNotValidException createExceptionWithFieldErrors(FieldError... fieldErrors) { + BindingResult bindingResult = new BeanPropertyBindingResult(new TodoDTO(), OBJECT_WITH_VALIDATION_ERROR); + + for (FieldError fieldError: fieldErrors) { + bindingResult.addError(fieldError); + } + + return new MethodArgumentNotValidException(mock(MethodParameter.class), bindingResult); + } + + + private final class FieldErrorBuilder { + + private String defaultMessage; + private String[] errorCodes; + private String fieldName; + + private FieldErrorBuilder() {} + + private FieldErrorBuilder defaultMessage(String defaultMessage) { + this.defaultMessage = defaultMessage; + return this; + } + + private FieldErrorBuilder errorCodes(String... errorCodes) { + this.errorCodes = errorCodes; + return this; + } + + private FieldErrorBuilder fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + private FieldError build() { + return new FieldError(OBJECT_WITH_VALIDATION_ERROR, + fieldName, + null, + false, + errorCodes, + new Object[]{}, + defaultMessage + ); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java new file mode 100644 index 0000000..8ae069a --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/error/ValidationErrorDTOTest.java @@ -0,0 +1,132 @@ +package net.petrikainulainen.springdata.jpa.web.error; + +import com.nitorcreations.junit.runners.NestedRunner; +import net.petrikainulainen.springdata.jpa.web.error.FieldErrorDTO; +import net.petrikainulainen.springdata.jpa.web.error.ValidationErrorDTO; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class ValidationErrorDTOTest { + + private static final String FIELD = "field"; + private static final String MESSAGE = "message"; + + public class AddFieldError { + + public class WhenFieldIsInvalid { + + public class WhenFieldIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(null, MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(null, MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenFieldIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError("", MESSAGE); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError("", MESSAGE)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenMessageIsInvalid { + + public class WhenMessageIsNull { + + @Test(expected = NullPointerException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, null); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, null)); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + + public class WhenMessageIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, ""); + } + + @Test + public void shouldNotCreateNewFieldError() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + + catchThrowable(() -> validationErrors.addFieldError(FIELD, "")); + + assertThat(validationErrors.getFieldErrors()).isEmpty(); + } + } + } + + public class WhenFieldAndMessageAreValid { + + @Test + public void shouldCreateNewFieldErrorAndSetField() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getField()).isEqualTo(FIELD); + } + + @Test + public void shouldCreateNewFieldErrorAndSetMessage() { + ValidationErrorDTO validationErrors = new ValidationErrorDTO(); + validationErrors.addFieldError(FIELD, MESSAGE); + + List fieldErrors = validationErrors.getFieldErrors(); + assertThat(fieldErrors).hasSize(1); + + FieldErrorDTO fieldError = fieldErrors.iterator().next(); + + assertThat(fieldError.getMessage()).isEqualTo(MESSAGE); + } + } + } +} diff --git a/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java new file mode 100644 index 0000000..1928461 --- /dev/null +++ b/querydsl/src/test/java/net/petrikainulainen/springdata/jpa/web/security/UserDTOTest.java @@ -0,0 +1,102 @@ +package net.petrikainulainen.springdata.jpa.web.security; + +import com.nitorcreations.junit.runners.NestedRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Petri Kainulainen + */ +@RunWith(NestedRunner.class) +public class UserDTOTest { + + public class CreateNew { + + private final String ROLE_USER = UserRole.ROLE_USER.name(); + + public class WhenUsernameIsEmpty { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER); + new UserDTO("", authorities); + } + } + + public class WhenUserNameIsNotEmpty { + + private final String USERNAME = "username"; + + public class WhenUserHasNoGrantedAuthorities { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + new UserDTO(USERNAME, new ArrayList<>()); + } + } + + public class WhenUserHasOneGrantedAuthority { + + public class WhenGrantedAuthorityIsKnown { + + private Collection authorities; + + @Before + public void createKnownAuthority() { + authorities = createAuthorities(ROLE_USER); + } + + @Test + public void shouldSetUsername() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getUsername()).isEqualTo(USERNAME); + } + + @Test + public void shouldSetRole() { + UserDTO user = new UserDTO(USERNAME, authorities); + assertThat(user.getRole()).isEqualTo(UserRole.ROLE_USER); + } + } + + public class WhenGrantedAuthorityIsUnknown { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities("UNKNOWN_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + + public class WhenUserHasMoreThanOneGrantedAuthority { + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowException() { + Collection authorities = createAuthorities(ROLE_USER, "ANOTHER_ROLE"); + new UserDTO(USERNAME, authorities); + } + } + } + } + + private Collection createAuthorities(String... roles) { + List authorities = new ArrayList<>(); + + for (String role: roles) { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); + authorities.add(authority); + } + + return authorities; + } +} diff --git a/tutorial-part-five/README b/tutorial-part-five/README deleted file mode 100644 index 34b9bac..0000000 --- a/tutorial-part-five/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Five: QueryDSL - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-five-querydsl/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-five/pom.xml b/tutorial-part-five/pom.xml deleted file mode 100644 index 9a94c35..0000000 --- a/tutorial-part-five/pom.xml +++ /dev/null @@ -1,278 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-five - war - 0.1 - Spring Data JPA Tutorial Part Five - Spring Data JPA Tutorial Part Five - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - 2.3.2 - - - - - commons-lang - commons-lang - 2.6 - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - com.mysema.querydsl - querydsl-core - ${querydsl.version} - - - - com.mysema.querydsl - querydsl-apt - ${querydsl.version} - - - - com.mysema.querydsl - querydsl-jpa - ${querydsl.version} - - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - org.mockito - mockito-core - 1.8.5 - test - - - org.springframework - spring-test - ${spring.version} - test - - - - data-jpa-tutorial-part-two - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - com.mysema.maven - maven-apt-plugin - 1.0.2 - - - generate-sources - - process - - - - target/generated-sources - - com.mysema.query.apt.jpa.JPAAnnotationProcessor - - - - - - - diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index 7eed429..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * - * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller", - "net.petrikainulainen.spring.datajpa.service"}) -@EnableTransactionManagement -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java deleted file mode 100644 index 83ed0b6..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import java.util.Locale; - -/** - * An abstract controller class which provides utility methods useful - * to actual controller classes. - * @author Petri Kainulainen - */ -public abstract class AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractController.class); - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - @Resource - private MessageSource messageSource; - - /** - * Adds a new error message - * @param model A model which stores the the error message. - * @param code A message code which is used to fetch the correct message from the message source. - * @param params The parameters attached to the actual error message. - */ - protected void addErrorMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("adding error message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedErrorMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedErrorMessage); - model.addFlashAttribute(FLASH_ERROR_MESSAGE, localizedErrorMessage); - } - - /** - * Adds a new feedback message. - * @param model A model which stores the feedback message. - * @param code A message code which is used to fetch the actual message from the message source. - * @param params The parameters which are attached to the actual feedback message. - */ - protected void addFeedbackMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("Adding feedback message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedFeedbackMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedFeedbackMessage); - model.addFlashAttribute(FLASH_FEEDBACK_MESSAGE, localizedFeedbackMessage); - } - - /** - * Creates a redirect view path for a specific controller action - * @param path The path processed by the controller method. - * @return A redirect view path to the given controller method. - */ - protected String createRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * This method should only be used by unit tests. - * @param messageSource - */ - protected void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java deleted file mode 100644 index b3de94b..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java +++ /dev/null @@ -1,209 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.util.List; - -/** - * @author Petri Kainulainen - */ -@Controller -@SessionAttributes("person") -public class PersonController extends AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); - - protected static final String ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND = "error.message.deleted.not.found"; - protected static final String ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND = "error.message.edited.not.found"; - - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_CREATED = "feedback.message.person.created"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_DELETED = "feedback.message.person.deleted"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_EDITED = "feedback.message.person.edited"; - - protected static final String MODEL_ATTIRUTE_PERSON = "person"; - protected static final String MODEL_ATTRIBUTE_PERSONS = "persons"; - protected static final String MODEL_ATTRIBUTE_SEARCH_CRITERIA = "searchCriteria"; - - protected static final String PERSON_ADD_FORM_VIEW = "person/create"; - protected static final String PERSON_EDIT_FORM_VIEW = "person/edit"; - protected static final String PERSON_LIST_VIEW = "person/list"; - protected static final String PERSON_SEARCH_RESULT_VIEW = "person/searchResults"; - - protected static final String REQUEST_MAPPING_LIST = "/"; - - @Resource - private PersonService personService; - - /** - * Processes delete person requests. - * @param id The id of the deleted person. - * @param attributes - * @return - */ - @RequestMapping(value = "/person/delete/{id}", method = RequestMethod.GET) - public String delete(@PathVariable("id") Long id, RedirectAttributes attributes) { - LOGGER.debug("Deleting person with id: " + id); - - try { - Person deleted = personService.delete(id); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_DELETED, deleted.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes search person requests. - * @param searchCriteria The search criteria. - * @param model - * @return - */ - @RequestMapping(value = "/person/search", method = RequestMethod.POST) - public String search(@ModelAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA)SearchDTO searchCriteria, Model model) { - LOGGER.debug("Searching persons with search criteria: " + searchCriteria); - - String searchTerm = searchCriteria.getSearchTerm(); - List persons = personService.search(searchTerm); - LOGGER.debug("Found " + persons.size() + " persons"); - - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - - return PERSON_SEARCH_RESULT_VIEW; - } - - /** - * Processes create person requests. - * @param model - * @return The name of the create person form view. - */ - @RequestMapping(value = "/person/create", method = RequestMethod.GET) - public String showCreatePersonForm(Model model) { - LOGGER.debug("Rendering create person form"); - - model.addAttribute(MODEL_ATTIRUTE_PERSON, new PersonDTO()); - - return PERSON_ADD_FORM_VIEW; - } - - /** - * Processes the submissions of create person form. - * @param created The information of the created persons. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/create", method = RequestMethod.POST) - public String submitCreatePersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO created, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Create person form was submitted with information: " + created); - - if (bindingResult.hasErrors()) { - return PERSON_ADD_FORM_VIEW; - } - - Person person = personService.create(created); - - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_CREATED, person.getName()); - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes edit person requests. - * @param id The id of the edited person. - * @param model - * @param attributes - * @return The name of the edit person form view. - */ - @RequestMapping(value = "/person/edit/{id}", method = RequestMethod.GET) - public String showEditPersonForm(@PathVariable("id") Long id, Model model, RedirectAttributes attributes) { - LOGGER.debug("Rendering edit person form for person with id: " + id); - - Person person = personService.findById(id); - if (person == null) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - model.addAttribute(MODEL_ATTIRUTE_PERSON, constructFormObject(person)); - - return PERSON_EDIT_FORM_VIEW; - } - - /** - * Processes the submissions of edit person form. - * @param updated The information of the edited person. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/edit", method = RequestMethod.POST) - public String submitEditPersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO updated, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Edit person form was submitted with information: " + updated); - - if (bindingResult.hasErrors()) { - LOGGER.debug("Edit person form contains validation errors. Rendering form view."); - return PERSON_EDIT_FORM_VIEW; - } - - try { - Person person = personService.update(updated); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_EDITED, person.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person was found with id: " + updated.getId()); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - private PersonDTO constructFormObject(Person person) { - PersonDTO formObject = new PersonDTO(); - - formObject.setId(person.getId()); - formObject.setFirstName(person.getFirstName()); - formObject.setLastName(person.getLastName()); - - return formObject; - } - - /** - * Processes requests to home page which lists all available persons. - * @param model - * @return The name of the person list view. - */ - @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) - public String showList(Model model) { - LOGGER.debug("Rendering person list page"); - - List persons = personService.findAll(); - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - model.addAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA, new SearchDTO()); - - return PERSON_LIST_VIEW; - } - - /** - * This setter method should only be used by unit tests - * @param personService - */ - protected void setPersonService(PersonService personService) { - this.personService = personService; - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java deleted file mode 100644 index 881ddb6..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.hibernate.validator.constraints.NotEmpty; - - -/** - * A DTO object which is used as a form object - * in create person and edit person forms. - * @author Petri Kainulainen - */ -public class PersonDTO { - - private Long id; - - @NotEmpty - private String firstName; - - @NotEmpty - private String lastName; - - public PersonDTO() { - - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - 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; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java deleted file mode 100644 index 84c6cfb..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; - -/** - * @author Petri Kainulainen - */ -public class SearchDTO { - - private String searchTerm; - - public SearchDTO() { - - } - - public String getSearchTerm() { - return searchTerm; - } - - public void setSearchTerm(String searchTerm) { - this.searchTerm = searchTerm; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java deleted file mode 100644 index d59fcc3..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import javax.persistence.*; -import java.util.Date; - -/** - * An entity class which contains the information of a single person. - * @author Petri Kainulainen - */ -@Entity -@NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(?1)") -@Table(name = "persons") -public class Person { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "creation_time", nullable = false) - private Date creationTime; - - @Column(name = "first_name", nullable = false) - private String firstName; - - @Column(name = "last_name", nullable = false) - private String lastName; - - @Column(name = "modification_time", nullable = false) - private Date modificationTime; - - @Version - private long version = 0; - - public Long getId() { - return id; - } - - /** - * Gets a builder which is used to create Person objects. - * @param firstName The first name of the created user. - * @param lastName The last name of the created user. - * @return A new Builder instance. - */ - public static Builder getBuilder(String firstName, String lastName) { - return new Builder(firstName, lastName); - } - - public Date getCreationTime() { - return creationTime; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - /** - * Gets the full name of the person. - * @return The full name of the person. - */ - @Transient - public String getName() { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - public Date getModificationTime() { - return modificationTime; - } - - public long getVersion() { - return version; - } - - public void update(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - @PreUpdate - public void preUpdate() { - modificationTime = new Date(); - } - - @PrePersist - public void prePersist() { - Date now = new Date(); - creationTime = now; - modificationTime = now; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * A Builder class used to create new Person objects. - */ - public static class Builder { - Person built; - - /** - * Creates a new Builder instance. - * @param firstName The first name of the created Person object. - * @param lastName The last name of the created Person object. - */ - Builder(String firstName, String lastName) { - built = new Person(); - built.firstName = firstName; - built.lastName = lastName; - } - - /** - * Builds the new Person object. - * @return The created Person object. - */ - public Person build() { - return built; - } - } - - /** - * This setter method should only be used by unit tests. - * @param id - */ - protected void setId(Long id) { - this.id = id; - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java deleted file mode 100644 index bf9b7be..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import com.mysema.query.types.Predicate; -import net.petrikainulainen.spring.datajpa.model.QPerson; - -/** - * A class which is used to create Querydsl predicates. - * @author Petri Kainulainen - */ -public class PersonPredicates { - - public static Predicate lastNameIsLike(final String searchTerm) { - QPerson person = QPerson.person; - return person.lastName.startsWithIgnoreCase(searchTerm); - } -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java deleted file mode 100644 index ea52095..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.querydsl.QueryDslPredicateExecutor; - -/** - * Specifies methods used to obtain and modify person related information - * which is stored in the database. - * @author Petri Kainulainen - */ -public interface PersonRepository extends JpaRepository, QueryDslPredicateExecutor { - -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java deleted file mode 100644 index 35cbd2e..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -/** - * This exception is thrown if the wanted person is not found. - * @author Petri Kainulainen - */ -public class PersonNotFoundException extends Exception { -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java deleted file mode 100644 index b431b31..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; - -import java.util.List; - -/** - * Declares methods used to obtain and modify person information. - * @author Petri Kainulainen - */ -public interface PersonService { - - /** - * Creates a new person. - * @param created The information of the created person. - * @return The created person. - */ - public Person create(PersonDTO created); - - /** - * Deletes a person. - * @param personId The id of the deleted person. - * @return The deleted person. - * @throws PersonNotFoundException if no person is found with the given id. - */ - public Person delete(Long personId) throws PersonNotFoundException; - - /** - * Finds all persons. - * @return A list of persons. - */ - public List findAll(); - - /** - * Finds person by id. - * @param id The id of the wanted person. - * @return The found person. If no person is found, this method returns null. - */ - public Person findById(Long id); - - /** - * Searches persons by using the given search term as a parameter. - * @param searchTerm - * @return A list of persons whose last name begins with the given search term. If no persons is found, this method - * returns an empty list. This search is case insensitive. - */ - public List search(String searchTerm); - - /** - * Updates the information of a person. - * @param updated The information of the updated person. - * @return The updated person. - * @throws PersonNotFoundException if no person is found with given id. - */ - public Person update(PersonDTO updated) throws PersonNotFoundException; -} diff --git a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java b/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java deleted file mode 100644 index 133b42e..0000000 --- a/tutorial-part-five/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java +++ /dev/null @@ -1,112 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.List; - -import static net.petrikainulainen.spring.datajpa.repository.PersonPredicates.lastNameIsLike; - -/** - * This implementation of the PersonService interface communicates with - * the database by using a Spring Data JPA repository. - * @author Petri Kainulainen - */ -@Service -public class RepositoryPersonService implements PersonService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class); - - @Resource - private PersonRepository personRepository; - - @Transactional - @Override - public Person create(PersonDTO created) { - LOGGER.debug("Creating a new person with information: " + created); - - Person person = Person.getBuilder(created.getFirstName(), created.getLastName()).build(); - - return personRepository.save(person); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person delete(Long personId) throws PersonNotFoundException { - LOGGER.debug("Deleting person with id: " + personId); - - Person deleted = personRepository.findOne(personId); - - if (deleted == null) { - LOGGER.debug("No person found with id: " + personId); - throw new PersonNotFoundException(); - } - - personRepository.delete(deleted); - return deleted; - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - LOGGER.debug("Finding all persons"); - return personRepository.findAll(); - } - - @Transactional(readOnly = true) - @Override - public Person findById(Long id) { - LOGGER.debug("Finding person by id: " + id); - return personRepository.findOne(id); - } - - @Transactional(readOnly = true) - @Override - public List search(String searchTerm) { - LOGGER.debug("Searching persons with search term: " + searchTerm); - - //Passes the specification created by PersonPredicates class to the repository. - Iterable persons = personRepository.findAll(lastNameIsLike(searchTerm)); - return constructList(persons); - } - - private List constructList(Iterable persons) { - List list = new ArrayList(); - for (Person person: persons) { - list.add(person); - } - return list; - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person update(PersonDTO updated) throws PersonNotFoundException { - LOGGER.debug("Updating person with information: " + updated); - - Person person = personRepository.findOne(updated.getId()); - - if (person == null) { - LOGGER.debug("No person found with id: " + updated.getId()); - throw new PersonNotFoundException(); - } - - person.update(updated.getFirstName(), updated.getLastName()); - - return person; - } - - /** - * This setter method should be used only by unit tests. - * @param personRepository - */ - protected void setPersonRepository(PersonRepository personRepository) { - this.personRepository = personRepository; - } -} diff --git a/tutorial-part-five/src/main/resources/application.properties b/tutorial-part-five/src/main/resources/application.properties deleted file mode 100644 index 426c303..0000000 --- a/tutorial-part-five/src/main/resources/application.properties +++ /dev/null @@ -1,29 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.hbm2ddl.auto=create-drop -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-five/src/main/resources/applicationContext.xml b/tutorial-part-five/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-five/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-five/src/main/resources/i18n/messages.properties b/tutorial-part-five/src/main/resources/i18n/messages.properties deleted file mode 100644 index d89d992..0000000 --- a/tutorial-part-five/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,44 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part Two - - -person.list.link.label=View persons - -#Person list page -person.list.page.title=Persons -person.list.page.label.no.persons.found=No persons was found. -person.create.link.label=Create person -person.edit.link.label=Edit person -person.delete.link.label=Delete person -person.search.form.title=Search -person.search.form.submit.label=Search -person.search.searchterm.label=Search Term -person.search.result.page.title=Search Results for Search Term - -SearchType.METHOD_NAME=Method Name -SearchType.NAMED_QUERY=Named Query -SearchType.QUERY_ANNOTATION=Query Annotation - -#Create person page -person.create.page.title=Create Person -person.create.page.submit.label=Create - -#Edit person page -person.edit.page.title=Edit Person -person.edit.page.submit.label=Edit - -#General person labels -person.label.firstName=First name -person.label.lastName=Last name - -#Error messages -error.message.deleted.not.found=Deleted person was not found. -error.message.edited.not.found=Edited person was not found. - -#Feedback messages -feedback.message.person.created=Person with name {0} was created. -feedback.message.person.deleted=Person with name {0} was deleted. -feedback.message.person.edited=Person with name {0} was edited. - -#Validation error messages -NotEmpty.person.firstName=Enter first name -NotEmpty.person.lastName=Enter last name \ No newline at end of file diff --git a/tutorial-part-five/src/main/resources/webdefault.xml b/tutorial-part-five/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-five/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/create.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/create.jsp deleted file mode 100644 index 508cf83..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/create.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/edit.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/edit.jsp deleted file mode 100644 index c9d5b53..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/edit.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- - -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/list.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/list.jsp deleted file mode 100644 index df816d3..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/list.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -
- -
-
- -
-
-
- - - - \ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/navigation.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/navigation.jsp deleted file mode 100644 index b2ea406..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/navigation.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -
- | - -
diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/personList.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/personList.jsp deleted file mode 100644 index d9d5096..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/personList.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -

- - - - - - - - - - - - - - - - - - -
">">
-
- -

- -

-
\ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp deleted file mode 100644 index eb8ec7d..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> - -
- -
- - -
-
- "/> -
-
-
\ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp b/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp deleted file mode 100644 index b47b78d..0000000 --- a/tutorial-part-five/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - - -

:

- - - \ No newline at end of file diff --git a/tutorial-part-five/src/main/webapp/static/css/styles.css b/tutorial-part-five/src/main/webapp/static/css/styles.css deleted file mode 100644 index 5ac2da3..0000000 --- a/tutorial-part-five/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,31 +0,0 @@ -body { - font-family: Verdana -} - -.error { - color: #ff0000; -} - -.messageblock { - color: #000; - background-color: #cbf7c8; - border: 3px solid #3bdb2a; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} - -.errorblock { - color: #000; - background-color: #ffEEEE; - border: 3px solid #ff0000; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} \ No newline at end of file diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java deleted file mode 100644 index cfce15c..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.petrikainulainen.spring.datajpa.context; - - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * A test context which is used for unit testing controllers. - * @author Petri Kainulainen - */ -@Configuration -public class TestContext { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} \ No newline at end of file diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java deleted file mode 100644 index 2ca7fd7..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.Locale; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class AbstractControllerTest { - - private static final String ERROR_MESSAGE = "errorMessage"; - private static final String ERROR_MESSAGE_CODE = "errorMessageCode"; - private static final String FEEDBACK_MESSAGE = "feedbackMessage"; - private static final String FEEDBACK_MESSAGE_CODE = "feedbackMessageCode"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String REDIRECT_PATH = "/foo"; - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private TestController controller; - - private MessageSource messageSourceMock; - - @Before - public void setUp() { - controller = new TestController(); - - messageSourceMock = mock(MessageSource.class); - controller.setMessageSource(messageSourceMock); - } - - @Test - public void addErrorMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(ERROR_MESSAGE); - - controller.addErrorMessage(model, ERROR_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String errorMessage = (String) model.getFlashAttributes().get(FLASH_ERROR_MESSAGE); - assertEquals(ERROR_MESSAGE, errorMessage); - } - - @Test - public void addFeedbackMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - - controller.addFeedbackMessage(model, FEEDBACK_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String feedbackMessage = (String) model.getFlashAttributes().get(FLASH_FEEDBACK_MESSAGE); - assertEquals(FEEDBACK_MESSAGE, feedbackMessage); - } - - @Test - public void createRedirectViewPath() { - String redirectView = controller.createRedirectViewPath(REDIRECT_PATH); - String expectedView = buildExpectedRedirectViewPath(REDIRECT_PATH); - - verifyZeroInteractions(messageSourceMock); - assertEquals(expectedView, redirectView); - } - - private String buildExpectedRedirectViewPath(String redirectPath) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(redirectPath); - return builder.toString(); - } - - - private class TestController extends AbstractController { - - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java deleted file mode 100644 index e83178c..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.context.TestContext; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.context.MessageSource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.when; - -/** - * An abstract base class for all controller unit tests. - * @author Petri Kainulainen - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {TestContext.class}) -public abstract class AbstractTestController { - - protected static final String ERROR_MESSAGE = "errorMessage"; - protected static final String FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private MessageSource messageSourceMock; - - @Resource - private Validator validator; - - @Before - public void setUp() { - messageSourceMock = mock(MessageSource.class); - setUpTest(); - } - - protected abstract void setUpTest(); - - /** - * Asserts that an error message is present. - * @param model The model which is used to store the error message. - * @param messageCode The message code of the expected error message. - */ - protected void assertErrorMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_ERROR_MESSAGE); - } - - /** - * Asserts that a feedback message is present. - * @param model The model which is used to store the feedback message. - * @param messageCode - */ - protected void assertFeedbackMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_FEEDBACK_MESSAGE); - } - - private void assertFlashMessages(RedirectAttributes model, String messageCode, String flashMessageParameterName) { - Map flashMessages = model.getFlashAttributes(); - Object message = flashMessages.get(flashMessageParameterName); - assertNotNull(message); - flashMessages.remove(message); - assertTrue(flashMessages.isEmpty()); - - verify(messageSourceMock, times(1)).getMessage(eq(messageCode), any(Object[].class), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - } - - /** - * Asserts that the binding result contains specified field errors. - * @param result The binding result - * @param fieldNames The names which should have validation errors. - */ - protected void assertFieldErrors(BindingResult result, String... fieldNames) { - assertEquals(fieldNames.length, result.getFieldErrorCount()); - for (String fieldName : fieldNames) { - assertNotNull(result.getFieldError(fieldName)); - } - } - - /** - * Binds and validates the given form object. - * @param request The http servlet request object. - * @param formObject A form object. - * @return A binding result containing the outcome of binding and validation. - */ - protected BindingResult bindAndValidate(HttpServletRequest request, Object formObject) { - WebDataBinder binder = new WebDataBinder(formObject); - binder.setValidator(validator); - binder.bind(new MutablePropertyValues(request.getParameterMap())); - binder.getValidator().validate(binder.getTarget(), binder.getBindingResult()); - return binder.getBindingResult(); - } - - /** - * Creates an expected redirect view path. - * @param path The path to the requested view. - * @return The expected redirect view path. - */ - protected String createExpectedRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * Initializes the message source mock to return an error message when - * the error message code given as a a parameter is used to get message - * from message source. - * @param errorMessageCode The wanted error message code. - */ - protected void initMessageSourceForErrorMessage(String errorMessageCode) { - when(messageSourceMock.getMessage(eq(errorMessageCode), any(Object[].class), any(Locale.class))).thenReturn(ERROR_MESSAGE); - } - - /** - * Initializes the message source mock to return a feedback message when - * the feedback message code given as a parameter is used to get message - * from message source. - * @param feedbackMessageCode The wanted feedback message code. - */ - protected void initMessageSourceForFeedbackMessage(String feedbackMessageCode) { - when(messageSourceMock.getMessage(eq(feedbackMessageCode), any(Object[].class), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - } - - /** - * Returns the message source mock. - * @return - */ - protected MessageSource getMessageSourceMock() { - return messageSourceMock; - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java deleted file mode 100644 index c05bfa5..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java +++ /dev/null @@ -1,365 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.*; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonControllerTest extends AbstractTestController { - - private static final String FIELD_NAME_FIRST_NAME = "firstName"; - private static final String FIELD_NAME_LAST_NAME = "lastName"; - - private static final Long PERSON_ID = Long.valueOf(5); - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private static final String SEARCH_TERM = "foo"; - - private PersonController controller; - - private PersonService personServiceMock; - - @Override - public void setUpTest() { - controller = new PersonController(); - - controller.setMessageSource(getMessageSourceMock()); - - personServiceMock = mock(PersonService.class); - controller.setPersonService(personServiceMock); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.delete(PERSON_ID)).thenReturn(deleted); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personServiceMock.delete(PERSON_ID)).thenThrow(new PersonNotFoundException()); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void search() { - SearchDTO searchCriteria = createSearchDTO(); - List expected = new ArrayList(); - when(personServiceMock.search(searchCriteria.getSearchTerm())).thenReturn(expected); - - BindingAwareModelMap model = new BindingAwareModelMap(); - String view = controller.search(searchCriteria, model); - - verify(personServiceMock, times(1)).search(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_SEARCH_RESULT_VIEW, view); - List actual = (List) model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS); - assertEquals(expected, actual); - } - - private SearchDTO createSearchDTO() { - SearchDTO dto = new SearchDTO(); - dto.setSearchTerm(SEARCH_TERM); - return dto; - } - - @Test - public void showCreatePersonForm() { - Model model = new BindingAwareModelMap(); - - String view = controller.showCreatePersonForm(model); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - - PersonDTO added = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - assertNotNull(added); - - assertNull(added.getId()); - assertNull(added.getFirstName()); - assertNull(added.getLastName()); - } - - @Test - public void submitCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME, LAST_NAME); - Person model = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.create(created)).thenReturn(model); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - - String expectedViewPath = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedViewPath, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - } - - @Test - public void submitEmptyCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = new PersonDTO(); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyFirstName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, null, LAST_NAME); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyLastName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, null); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_LAST_NAME); - } - - @Test - public void showEditPersonForm() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.findById(PERSON_ID)).thenReturn(person); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - - PersonDTO formObject = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - - assertNotNull(formObject); - assertEquals(person.getId(), formObject.getId()); - assertEquals(person.getFirstName(), formObject.getFirstName()); - assertEquals(person.getLastName(), formObject.getLastName()); - } - - @Test - public void showEditPersonFormWhenPersonIsNotFound() { - when(personServiceMock.findById(PERSON_ID)).thenReturn(null); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEditPersonForm() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenReturn(person); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - assertEquals(updated.getFirstName(), person.getFirstName()); - assertEquals(updated.getLastName(), person.getLastName()); - } - - @Test - public void submitEditPersonFormWhenPersonIsNotFound() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenThrow(new PersonNotFoundException()); - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEmptyEditPersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitEditPersonFormWhenFirstNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, LAST_NAME_UPDATED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitEditPersonFormWhenLastNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_LAST_NAME); - } - - @Test - public void showList() { - List persons = new ArrayList(); - when(personServiceMock.findAll()).thenReturn(persons); - - Model model = new BindingAwareModelMap(); - String view = controller.showList(model); - - verify(personServiceMock, times(1)).findAll(); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_LIST_VIEW, view); - assertEquals(persons, model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS)); - - SearchDTO searchCriteria = (SearchDTO) model.asMap().get(PersonController.MODEL_ATTRIBUTE_SEARCH_CRITERIA); - assertNotNull(searchCriteria); - assertNull(searchCriteria.getSearchTerm()); - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java deleted file mode 100644 index de7a368..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.junit.Test; - -import java.util.Date; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Petri Kainulainen - */ -public class PersonTest { - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "Foo1"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "Bar1"; - - @Test - public void build() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - assertEquals(FIRST_NAME, built.getFirstName()); - assertEquals(LAST_NAME, built.getLastName()); - assertEquals(0, built.getVersion()); - - assertNull(built.getCreationTime()); - assertNull(built.getModificationTime()); - assertNull(built.getId()); - } - - @Test - public void getName() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - String expectedName = constructName(FIRST_NAME, LAST_NAME); - assertEquals(expectedName, built.getName()); - } - - private String constructName(String firstName, String lastName) { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - @Test - public void prePersist() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertEquals(creationTime, modificationTime); - } - - @Test - public void preUpdate() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //Back to work - } - - built.preUpdate(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertTrue(modificationTime.after(creationTime)); - } - - @Test - public void update() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.update(FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - assertEquals(FIRST_NAME_UPDATED, built.getFirstName()); - assertEquals(LAST_NAME_UPDATED, built.getLastName()); - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java deleted file mode 100644 index 6575587..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; - -/** - * An utility class which contains useful methods for unit testing person related - * functions. - * @author Petri Kainulainen - */ -public class PersonTestUtil { - - public static PersonDTO createDTO(Long id, String firstName, String lastName) { - PersonDTO dto = new PersonDTO(); - - dto.setId(id); - dto.setFirstName(firstName); - dto.setLastName(lastName); - - return dto; - } - - public static Person createModelObject(Long id, String firstName, String lastName) { - Person model = Person.getBuilder(firstName, lastName).build(); - - model.setId(id); - - return model; - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java deleted file mode 100644 index 608c693..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import com.mysema.query.types.Predicate; -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; - -/** - * @author Petri Kainulainen - */ -public class PersonPredicatesTest { - - private static final String SEARCH_TERM = "Foo"; - private static final String EXPECTED_PREDICATE_STRING = "startsWithIgnoreCase(person.lastName,Foo)"; - - @Test - public void lastNameLike() { - Predicate predicate = PersonPredicates.lastNameIsLike(SEARCH_TERM); - String predicateAsString = predicate.toString(); - assertEquals(EXPECTED_PREDICATE_STRING, predicateAsString); - } -} diff --git a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java b/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java deleted file mode 100644 index 5c6030c..0000000 --- a/tutorial-part-five/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import com.mysema.query.types.Predicate; -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class RepositoryPersonServiceTest { - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - private static final String SEARCH_TERM = "foo"; - - private RepositoryPersonService personService; - - private PersonRepository personRepositoryMock; - - @Before - public void setUp() { - personService = new RepositoryPersonService(); - - personRepositoryMock = mock(PersonRepository.class); - personService.setPersonRepository(personRepositoryMock); - } - - @Test - public void create() { - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, LAST_NAME); - Person persisted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.save(any(Person.class))).thenReturn(persisted); - - Person returned = personService.create(created); - - ArgumentCaptor personArgument = ArgumentCaptor.forClass(Person.class); - verify(personRepositoryMock, times(1)).save(personArgument.capture()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(created, personArgument.getValue()); - assertEquals(persisted, returned); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(deleted); - - Person returned = personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verify(personRepositoryMock, times(1)).delete(deleted); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(deleted, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(null); - - personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - } - - @Test - public void findAll() { - List persons = new ArrayList(); - when(personRepositoryMock.findAll()).thenReturn(persons); - - List returned = personService.findAll(); - - verify(personRepositoryMock, times(1)).findAll(); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(persons, returned); - } - - @Test - public void findById() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(person); - - Person returned = personService.findById(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(person, returned); - } - - @Test - public void search() { - List expected = new ArrayList(); - when(personRepositoryMock.findAll(any(Predicate.class))).thenReturn(expected); - - List actual = personService.search(SEARCH_TERM); - - verify(personRepositoryMock, times(1)).findAll(any(Predicate.class)); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(expected, actual); - } - - @Test - public void update() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(person); - - Person returned = personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(updated, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void updateWhenPersonIsNotFound() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(null); - - personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - } - - private void assertPerson(PersonDTO expected, Person actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getFirstName(), actual.getFirstName()); - assertEquals(expected.getLastName(), expected.getLastName()); - } - -} diff --git a/tutorial-part-four/README b/tutorial-part-four/README deleted file mode 100644 index cfc2fb0..0000000 --- a/tutorial-part-four/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Four: JPA Criteria Queries - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-four-jpa-criteria-queries/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-four/pom.xml b/tutorial-part-four/pom.xml deleted file mode 100644 index af00050..0000000 --- a/tutorial-part-four/pom.xml +++ /dev/null @@ -1,239 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-four - war - 0.1 - Spring Data JPA Tutorial Part Four - Spring Data JPA Tutorial Part Four - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - - - - - commons-lang - commons-lang - 2.6 - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - org.mockito - mockito-core - 1.8.5 - test - - - org.springframework - spring-test - ${spring.version} - test - - - - data-jpa-tutorial-part-two - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index 7eed429..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * - * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller", - "net.petrikainulainen.spring.datajpa.service"}) -@EnableTransactionManagement -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java deleted file mode 100644 index 83ed0b6..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import java.util.Locale; - -/** - * An abstract controller class which provides utility methods useful - * to actual controller classes. - * @author Petri Kainulainen - */ -public abstract class AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractController.class); - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - @Resource - private MessageSource messageSource; - - /** - * Adds a new error message - * @param model A model which stores the the error message. - * @param code A message code which is used to fetch the correct message from the message source. - * @param params The parameters attached to the actual error message. - */ - protected void addErrorMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("adding error message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedErrorMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedErrorMessage); - model.addFlashAttribute(FLASH_ERROR_MESSAGE, localizedErrorMessage); - } - - /** - * Adds a new feedback message. - * @param model A model which stores the feedback message. - * @param code A message code which is used to fetch the actual message from the message source. - * @param params The parameters which are attached to the actual feedback message. - */ - protected void addFeedbackMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("Adding feedback message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedFeedbackMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedFeedbackMessage); - model.addFlashAttribute(FLASH_FEEDBACK_MESSAGE, localizedFeedbackMessage); - } - - /** - * Creates a redirect view path for a specific controller action - * @param path The path processed by the controller method. - * @return A redirect view path to the given controller method. - */ - protected String createRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * This method should only be used by unit tests. - * @param messageSource - */ - protected void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java deleted file mode 100644 index b3de94b..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java +++ /dev/null @@ -1,209 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.util.List; - -/** - * @author Petri Kainulainen - */ -@Controller -@SessionAttributes("person") -public class PersonController extends AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); - - protected static final String ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND = "error.message.deleted.not.found"; - protected static final String ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND = "error.message.edited.not.found"; - - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_CREATED = "feedback.message.person.created"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_DELETED = "feedback.message.person.deleted"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_EDITED = "feedback.message.person.edited"; - - protected static final String MODEL_ATTIRUTE_PERSON = "person"; - protected static final String MODEL_ATTRIBUTE_PERSONS = "persons"; - protected static final String MODEL_ATTRIBUTE_SEARCH_CRITERIA = "searchCriteria"; - - protected static final String PERSON_ADD_FORM_VIEW = "person/create"; - protected static final String PERSON_EDIT_FORM_VIEW = "person/edit"; - protected static final String PERSON_LIST_VIEW = "person/list"; - protected static final String PERSON_SEARCH_RESULT_VIEW = "person/searchResults"; - - protected static final String REQUEST_MAPPING_LIST = "/"; - - @Resource - private PersonService personService; - - /** - * Processes delete person requests. - * @param id The id of the deleted person. - * @param attributes - * @return - */ - @RequestMapping(value = "/person/delete/{id}", method = RequestMethod.GET) - public String delete(@PathVariable("id") Long id, RedirectAttributes attributes) { - LOGGER.debug("Deleting person with id: " + id); - - try { - Person deleted = personService.delete(id); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_DELETED, deleted.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes search person requests. - * @param searchCriteria The search criteria. - * @param model - * @return - */ - @RequestMapping(value = "/person/search", method = RequestMethod.POST) - public String search(@ModelAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA)SearchDTO searchCriteria, Model model) { - LOGGER.debug("Searching persons with search criteria: " + searchCriteria); - - String searchTerm = searchCriteria.getSearchTerm(); - List persons = personService.search(searchTerm); - LOGGER.debug("Found " + persons.size() + " persons"); - - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - - return PERSON_SEARCH_RESULT_VIEW; - } - - /** - * Processes create person requests. - * @param model - * @return The name of the create person form view. - */ - @RequestMapping(value = "/person/create", method = RequestMethod.GET) - public String showCreatePersonForm(Model model) { - LOGGER.debug("Rendering create person form"); - - model.addAttribute(MODEL_ATTIRUTE_PERSON, new PersonDTO()); - - return PERSON_ADD_FORM_VIEW; - } - - /** - * Processes the submissions of create person form. - * @param created The information of the created persons. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/create", method = RequestMethod.POST) - public String submitCreatePersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO created, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Create person form was submitted with information: " + created); - - if (bindingResult.hasErrors()) { - return PERSON_ADD_FORM_VIEW; - } - - Person person = personService.create(created); - - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_CREATED, person.getName()); - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes edit person requests. - * @param id The id of the edited person. - * @param model - * @param attributes - * @return The name of the edit person form view. - */ - @RequestMapping(value = "/person/edit/{id}", method = RequestMethod.GET) - public String showEditPersonForm(@PathVariable("id") Long id, Model model, RedirectAttributes attributes) { - LOGGER.debug("Rendering edit person form for person with id: " + id); - - Person person = personService.findById(id); - if (person == null) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - model.addAttribute(MODEL_ATTIRUTE_PERSON, constructFormObject(person)); - - return PERSON_EDIT_FORM_VIEW; - } - - /** - * Processes the submissions of edit person form. - * @param updated The information of the edited person. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/edit", method = RequestMethod.POST) - public String submitEditPersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO updated, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Edit person form was submitted with information: " + updated); - - if (bindingResult.hasErrors()) { - LOGGER.debug("Edit person form contains validation errors. Rendering form view."); - return PERSON_EDIT_FORM_VIEW; - } - - try { - Person person = personService.update(updated); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_EDITED, person.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person was found with id: " + updated.getId()); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - private PersonDTO constructFormObject(Person person) { - PersonDTO formObject = new PersonDTO(); - - formObject.setId(person.getId()); - formObject.setFirstName(person.getFirstName()); - formObject.setLastName(person.getLastName()); - - return formObject; - } - - /** - * Processes requests to home page which lists all available persons. - * @param model - * @return The name of the person list view. - */ - @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) - public String showList(Model model) { - LOGGER.debug("Rendering person list page"); - - List persons = personService.findAll(); - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - model.addAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA, new SearchDTO()); - - return PERSON_LIST_VIEW; - } - - /** - * This setter method should only be used by unit tests - * @param personService - */ - protected void setPersonService(PersonService personService) { - this.personService = personService; - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java deleted file mode 100644 index 881ddb6..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.hibernate.validator.constraints.NotEmpty; - - -/** - * A DTO object which is used as a form object - * in create person and edit person forms. - * @author Petri Kainulainen - */ -public class PersonDTO { - - private Long id; - - @NotEmpty - private String firstName; - - @NotEmpty - private String lastName; - - public PersonDTO() { - - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - 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; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java deleted file mode 100644 index 84c6cfb..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; - -/** - * @author Petri Kainulainen - */ -public class SearchDTO { - - private String searchTerm; - - public SearchDTO() { - - } - - public String getSearchTerm() { - return searchTerm; - } - - public void setSearchTerm(String searchTerm) { - this.searchTerm = searchTerm; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java deleted file mode 100644 index d59fcc3..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import javax.persistence.*; -import java.util.Date; - -/** - * An entity class which contains the information of a single person. - * @author Petri Kainulainen - */ -@Entity -@NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(?1)") -@Table(name = "persons") -public class Person { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "creation_time", nullable = false) - private Date creationTime; - - @Column(name = "first_name", nullable = false) - private String firstName; - - @Column(name = "last_name", nullable = false) - private String lastName; - - @Column(name = "modification_time", nullable = false) - private Date modificationTime; - - @Version - private long version = 0; - - public Long getId() { - return id; - } - - /** - * Gets a builder which is used to create Person objects. - * @param firstName The first name of the created user. - * @param lastName The last name of the created user. - * @return A new Builder instance. - */ - public static Builder getBuilder(String firstName, String lastName) { - return new Builder(firstName, lastName); - } - - public Date getCreationTime() { - return creationTime; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - /** - * Gets the full name of the person. - * @return The full name of the person. - */ - @Transient - public String getName() { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - public Date getModificationTime() { - return modificationTime; - } - - public long getVersion() { - return version; - } - - public void update(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - @PreUpdate - public void preUpdate() { - modificationTime = new Date(); - } - - @PrePersist - public void prePersist() { - Date now = new Date(); - creationTime = now; - modificationTime = now; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * A Builder class used to create new Person objects. - */ - public static class Builder { - Person built; - - /** - * Creates a new Builder instance. - * @param firstName The first name of the created Person object. - * @param lastName The last name of the created Person object. - */ - Builder(String firstName, String lastName) { - built = new Person(); - built.firstName = firstName; - built.lastName = lastName; - } - - /** - * Builds the new Person object. - * @return The created Person object. - */ - public Person build() { - return built; - } - } - - /** - * This setter method should only be used by unit tests. - * @param id - */ - protected void setId(Long id) { - this.id = id; - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person_.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person_.java deleted file mode 100644 index cb413c2..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/model/Person_.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import javax.persistence.metamodel.SingularAttribute; -import javax.persistence.metamodel.StaticMetamodel; - -/** - * A meta model class used to create type safe queries from person - * information. - * @author Petri Kainulainen - */ -@StaticMetamodel(Person.class) -public class Person_ { - public static volatile SingularAttribute lastName; -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java deleted file mode 100644 index a127121..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; - -/** - * Specifies methods used to obtain and modify person related information - * which is stored in the database. - * @author Petri Kainulainen - */ -public interface PersonRepository extends JpaRepository, JpaSpecificationExecutor { - -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecifications.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecifications.java deleted file mode 100644 index 5871d14..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecifications.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.Person_; -import org.springframework.data.jpa.domain.Specification; - -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; - -/** - * A class which is used to create Specification objects which are used - * to create JPA criteria queries for person information. - * @author Petri Kainulainen - */ -public class PersonSpecifications { - - /** - * Creates a specification used to find persons whose last name begins with - * the given search term. This search is case insensitive. - * @param searchTerm - * @return - */ - public static Specification lastNameIsLike(final String searchTerm) { - - return new Specification() { - @Override - public Predicate toPredicate(Root personRoot, CriteriaQuery query, CriteriaBuilder cb) { - String likePattern = getLikePattern(searchTerm); - return cb.like(cb.lower(personRoot.get(Person_.lastName)), likePattern); - } - - private String getLikePattern(final String searchTerm) { - StringBuilder pattern = new StringBuilder(); - pattern.append(searchTerm.toLowerCase()); - pattern.append("%"); - return pattern.toString(); - } - }; - } -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java deleted file mode 100644 index 35cbd2e..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -/** - * This exception is thrown if the wanted person is not found. - * @author Petri Kainulainen - */ -public class PersonNotFoundException extends Exception { -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java deleted file mode 100644 index b431b31..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; - -import java.util.List; - -/** - * Declares methods used to obtain and modify person information. - * @author Petri Kainulainen - */ -public interface PersonService { - - /** - * Creates a new person. - * @param created The information of the created person. - * @return The created person. - */ - public Person create(PersonDTO created); - - /** - * Deletes a person. - * @param personId The id of the deleted person. - * @return The deleted person. - * @throws PersonNotFoundException if no person is found with the given id. - */ - public Person delete(Long personId) throws PersonNotFoundException; - - /** - * Finds all persons. - * @return A list of persons. - */ - public List findAll(); - - /** - * Finds person by id. - * @param id The id of the wanted person. - * @return The found person. If no person is found, this method returns null. - */ - public Person findById(Long id); - - /** - * Searches persons by using the given search term as a parameter. - * @param searchTerm - * @return A list of persons whose last name begins with the given search term. If no persons is found, this method - * returns an empty list. This search is case insensitive. - */ - public List search(String searchTerm); - - /** - * Updates the information of a person. - * @param updated The information of the updated person. - * @return The updated person. - * @throws PersonNotFoundException if no person is found with given id. - */ - public Person update(PersonDTO updated) throws PersonNotFoundException; -} diff --git a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java b/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java deleted file mode 100644 index 9e9274d..0000000 --- a/tutorial-part-four/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java +++ /dev/null @@ -1,102 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.List; - -import static net.petrikainulainen.spring.datajpa.repository.PersonSpecifications.lastNameIsLike; - -/** - * This implementation of the PersonService interface communicates with - * the database by using a Spring Data JPA repository. - * @author Petri Kainulainen - */ -@Service -public class RepositoryPersonService implements PersonService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class); - - @Resource - private PersonRepository personRepository; - - @Transactional - @Override - public Person create(PersonDTO created) { - LOGGER.debug("Creating a new person with information: " + created); - - Person person = Person.getBuilder(created.getFirstName(), created.getLastName()).build(); - - return personRepository.save(person); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person delete(Long personId) throws PersonNotFoundException { - LOGGER.debug("Deleting person with id: " + personId); - - Person deleted = personRepository.findOne(personId); - - if (deleted == null) { - LOGGER.debug("No person found with id: " + personId); - throw new PersonNotFoundException(); - } - - personRepository.delete(deleted); - return deleted; - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - LOGGER.debug("Finding all persons"); - return personRepository.findAll(); - } - - @Transactional(readOnly = true) - @Override - public Person findById(Long id) { - LOGGER.debug("Finding person by id: " + id); - return personRepository.findOne(id); - } - - @Transactional(readOnly = true) - @Override - public List search(String searchTerm) { - LOGGER.debug("Searching persons with search term: " + searchTerm); - - //Passes the specification created by PersonSpecifications class to the repository. - return personRepository.findAll(lastNameIsLike(searchTerm)); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person update(PersonDTO updated) throws PersonNotFoundException { - LOGGER.debug("Updating person with information: " + updated); - - Person person = personRepository.findOne(updated.getId()); - - if (person == null) { - LOGGER.debug("No person found with id: " + updated.getId()); - throw new PersonNotFoundException(); - } - - person.update(updated.getFirstName(), updated.getLastName()); - - return person; - } - - /** - * This setter method should be used only by unit tests. - * @param personRepository - */ - protected void setPersonRepository(PersonRepository personRepository) { - this.personRepository = personRepository; - } -} diff --git a/tutorial-part-four/src/main/resources/application.properties b/tutorial-part-four/src/main/resources/application.properties deleted file mode 100644 index 426c303..0000000 --- a/tutorial-part-four/src/main/resources/application.properties +++ /dev/null @@ -1,29 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.hbm2ddl.auto=create-drop -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-four/src/main/resources/applicationContext.xml b/tutorial-part-four/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-four/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-four/src/main/resources/i18n/messages.properties b/tutorial-part-four/src/main/resources/i18n/messages.properties deleted file mode 100644 index d89d992..0000000 --- a/tutorial-part-four/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,44 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part Two - - -person.list.link.label=View persons - -#Person list page -person.list.page.title=Persons -person.list.page.label.no.persons.found=No persons was found. -person.create.link.label=Create person -person.edit.link.label=Edit person -person.delete.link.label=Delete person -person.search.form.title=Search -person.search.form.submit.label=Search -person.search.searchterm.label=Search Term -person.search.result.page.title=Search Results for Search Term - -SearchType.METHOD_NAME=Method Name -SearchType.NAMED_QUERY=Named Query -SearchType.QUERY_ANNOTATION=Query Annotation - -#Create person page -person.create.page.title=Create Person -person.create.page.submit.label=Create - -#Edit person page -person.edit.page.title=Edit Person -person.edit.page.submit.label=Edit - -#General person labels -person.label.firstName=First name -person.label.lastName=Last name - -#Error messages -error.message.deleted.not.found=Deleted person was not found. -error.message.edited.not.found=Edited person was not found. - -#Feedback messages -feedback.message.person.created=Person with name {0} was created. -feedback.message.person.deleted=Person with name {0} was deleted. -feedback.message.person.edited=Person with name {0} was edited. - -#Validation error messages -NotEmpty.person.firstName=Enter first name -NotEmpty.person.lastName=Enter last name \ No newline at end of file diff --git a/tutorial-part-four/src/main/resources/webdefault.xml b/tutorial-part-four/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-four/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/create.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/create.jsp deleted file mode 100644 index 508cf83..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/create.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/edit.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/edit.jsp deleted file mode 100644 index c9d5b53..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/edit.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- - -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/list.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/list.jsp deleted file mode 100644 index df816d3..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/list.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -
- -
-
- -
-
-
- - - - \ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/navigation.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/navigation.jsp deleted file mode 100644 index b2ea406..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/navigation.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -
- | - -
diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/personList.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/personList.jsp deleted file mode 100644 index d9d5096..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/personList.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -

- - - - - - - - - - - - - - - - - - -
">">
-
- -

- -

-
\ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp deleted file mode 100644 index eb8ec7d..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> - -
- -
- - -
-
- "/> -
-
-
\ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp b/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp deleted file mode 100644 index b47b78d..0000000 --- a/tutorial-part-four/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - - -

:

- - - \ No newline at end of file diff --git a/tutorial-part-four/src/main/webapp/static/css/styles.css b/tutorial-part-four/src/main/webapp/static/css/styles.css deleted file mode 100644 index 5ac2da3..0000000 --- a/tutorial-part-four/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,31 +0,0 @@ -body { - font-family: Verdana -} - -.error { - color: #ff0000; -} - -.messageblock { - color: #000; - background-color: #cbf7c8; - border: 3px solid #3bdb2a; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} - -.errorblock { - color: #000; - background-color: #ffEEEE; - border: 3px solid #ff0000; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} \ No newline at end of file diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java deleted file mode 100644 index cfce15c..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.petrikainulainen.spring.datajpa.context; - - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * A test context which is used for unit testing controllers. - * @author Petri Kainulainen - */ -@Configuration -public class TestContext { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} \ No newline at end of file diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java deleted file mode 100644 index 2ca7fd7..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.Locale; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class AbstractControllerTest { - - private static final String ERROR_MESSAGE = "errorMessage"; - private static final String ERROR_MESSAGE_CODE = "errorMessageCode"; - private static final String FEEDBACK_MESSAGE = "feedbackMessage"; - private static final String FEEDBACK_MESSAGE_CODE = "feedbackMessageCode"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String REDIRECT_PATH = "/foo"; - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private TestController controller; - - private MessageSource messageSourceMock; - - @Before - public void setUp() { - controller = new TestController(); - - messageSourceMock = mock(MessageSource.class); - controller.setMessageSource(messageSourceMock); - } - - @Test - public void addErrorMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(ERROR_MESSAGE); - - controller.addErrorMessage(model, ERROR_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String errorMessage = (String) model.getFlashAttributes().get(FLASH_ERROR_MESSAGE); - assertEquals(ERROR_MESSAGE, errorMessage); - } - - @Test - public void addFeedbackMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - - controller.addFeedbackMessage(model, FEEDBACK_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String feedbackMessage = (String) model.getFlashAttributes().get(FLASH_FEEDBACK_MESSAGE); - assertEquals(FEEDBACK_MESSAGE, feedbackMessage); - } - - @Test - public void createRedirectViewPath() { - String redirectView = controller.createRedirectViewPath(REDIRECT_PATH); - String expectedView = buildExpectedRedirectViewPath(REDIRECT_PATH); - - verifyZeroInteractions(messageSourceMock); - assertEquals(expectedView, redirectView); - } - - private String buildExpectedRedirectViewPath(String redirectPath) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(redirectPath); - return builder.toString(); - } - - - private class TestController extends AbstractController { - - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java deleted file mode 100644 index e83178c..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.context.TestContext; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.context.MessageSource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.when; - -/** - * An abstract base class for all controller unit tests. - * @author Petri Kainulainen - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {TestContext.class}) -public abstract class AbstractTestController { - - protected static final String ERROR_MESSAGE = "errorMessage"; - protected static final String FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private MessageSource messageSourceMock; - - @Resource - private Validator validator; - - @Before - public void setUp() { - messageSourceMock = mock(MessageSource.class); - setUpTest(); - } - - protected abstract void setUpTest(); - - /** - * Asserts that an error message is present. - * @param model The model which is used to store the error message. - * @param messageCode The message code of the expected error message. - */ - protected void assertErrorMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_ERROR_MESSAGE); - } - - /** - * Asserts that a feedback message is present. - * @param model The model which is used to store the feedback message. - * @param messageCode - */ - protected void assertFeedbackMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_FEEDBACK_MESSAGE); - } - - private void assertFlashMessages(RedirectAttributes model, String messageCode, String flashMessageParameterName) { - Map flashMessages = model.getFlashAttributes(); - Object message = flashMessages.get(flashMessageParameterName); - assertNotNull(message); - flashMessages.remove(message); - assertTrue(flashMessages.isEmpty()); - - verify(messageSourceMock, times(1)).getMessage(eq(messageCode), any(Object[].class), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - } - - /** - * Asserts that the binding result contains specified field errors. - * @param result The binding result - * @param fieldNames The names which should have validation errors. - */ - protected void assertFieldErrors(BindingResult result, String... fieldNames) { - assertEquals(fieldNames.length, result.getFieldErrorCount()); - for (String fieldName : fieldNames) { - assertNotNull(result.getFieldError(fieldName)); - } - } - - /** - * Binds and validates the given form object. - * @param request The http servlet request object. - * @param formObject A form object. - * @return A binding result containing the outcome of binding and validation. - */ - protected BindingResult bindAndValidate(HttpServletRequest request, Object formObject) { - WebDataBinder binder = new WebDataBinder(formObject); - binder.setValidator(validator); - binder.bind(new MutablePropertyValues(request.getParameterMap())); - binder.getValidator().validate(binder.getTarget(), binder.getBindingResult()); - return binder.getBindingResult(); - } - - /** - * Creates an expected redirect view path. - * @param path The path to the requested view. - * @return The expected redirect view path. - */ - protected String createExpectedRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * Initializes the message source mock to return an error message when - * the error message code given as a a parameter is used to get message - * from message source. - * @param errorMessageCode The wanted error message code. - */ - protected void initMessageSourceForErrorMessage(String errorMessageCode) { - when(messageSourceMock.getMessage(eq(errorMessageCode), any(Object[].class), any(Locale.class))).thenReturn(ERROR_MESSAGE); - } - - /** - * Initializes the message source mock to return a feedback message when - * the feedback message code given as a parameter is used to get message - * from message source. - * @param feedbackMessageCode The wanted feedback message code. - */ - protected void initMessageSourceForFeedbackMessage(String feedbackMessageCode) { - when(messageSourceMock.getMessage(eq(feedbackMessageCode), any(Object[].class), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - } - - /** - * Returns the message source mock. - * @return - */ - protected MessageSource getMessageSourceMock() { - return messageSourceMock; - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java deleted file mode 100644 index c05bfa5..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java +++ /dev/null @@ -1,365 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.*; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonControllerTest extends AbstractTestController { - - private static final String FIELD_NAME_FIRST_NAME = "firstName"; - private static final String FIELD_NAME_LAST_NAME = "lastName"; - - private static final Long PERSON_ID = Long.valueOf(5); - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private static final String SEARCH_TERM = "foo"; - - private PersonController controller; - - private PersonService personServiceMock; - - @Override - public void setUpTest() { - controller = new PersonController(); - - controller.setMessageSource(getMessageSourceMock()); - - personServiceMock = mock(PersonService.class); - controller.setPersonService(personServiceMock); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.delete(PERSON_ID)).thenReturn(deleted); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personServiceMock.delete(PERSON_ID)).thenThrow(new PersonNotFoundException()); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void search() { - SearchDTO searchCriteria = createSearchDTO(); - List expected = new ArrayList(); - when(personServiceMock.search(searchCriteria.getSearchTerm())).thenReturn(expected); - - BindingAwareModelMap model = new BindingAwareModelMap(); - String view = controller.search(searchCriteria, model); - - verify(personServiceMock, times(1)).search(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_SEARCH_RESULT_VIEW, view); - List actual = (List) model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS); - assertEquals(expected, actual); - } - - private SearchDTO createSearchDTO() { - SearchDTO dto = new SearchDTO(); - dto.setSearchTerm(SEARCH_TERM); - return dto; - } - - @Test - public void showCreatePersonForm() { - Model model = new BindingAwareModelMap(); - - String view = controller.showCreatePersonForm(model); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - - PersonDTO added = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - assertNotNull(added); - - assertNull(added.getId()); - assertNull(added.getFirstName()); - assertNull(added.getLastName()); - } - - @Test - public void submitCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME, LAST_NAME); - Person model = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.create(created)).thenReturn(model); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - - String expectedViewPath = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedViewPath, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - } - - @Test - public void submitEmptyCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = new PersonDTO(); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyFirstName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, null, LAST_NAME); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyLastName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, null); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_LAST_NAME); - } - - @Test - public void showEditPersonForm() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.findById(PERSON_ID)).thenReturn(person); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - - PersonDTO formObject = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - - assertNotNull(formObject); - assertEquals(person.getId(), formObject.getId()); - assertEquals(person.getFirstName(), formObject.getFirstName()); - assertEquals(person.getLastName(), formObject.getLastName()); - } - - @Test - public void showEditPersonFormWhenPersonIsNotFound() { - when(personServiceMock.findById(PERSON_ID)).thenReturn(null); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEditPersonForm() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenReturn(person); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - assertEquals(updated.getFirstName(), person.getFirstName()); - assertEquals(updated.getLastName(), person.getLastName()); - } - - @Test - public void submitEditPersonFormWhenPersonIsNotFound() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenThrow(new PersonNotFoundException()); - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEmptyEditPersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitEditPersonFormWhenFirstNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, LAST_NAME_UPDATED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitEditPersonFormWhenLastNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_LAST_NAME); - } - - @Test - public void showList() { - List persons = new ArrayList(); - when(personServiceMock.findAll()).thenReturn(persons); - - Model model = new BindingAwareModelMap(); - String view = controller.showList(model); - - verify(personServiceMock, times(1)).findAll(); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_LIST_VIEW, view); - assertEquals(persons, model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS)); - - SearchDTO searchCriteria = (SearchDTO) model.asMap().get(PersonController.MODEL_ATTRIBUTE_SEARCH_CRITERIA); - assertNotNull(searchCriteria); - assertNull(searchCriteria.getSearchTerm()); - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java deleted file mode 100644 index de7a368..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.junit.Test; - -import java.util.Date; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Petri Kainulainen - */ -public class PersonTest { - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "Foo1"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "Bar1"; - - @Test - public void build() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - assertEquals(FIRST_NAME, built.getFirstName()); - assertEquals(LAST_NAME, built.getLastName()); - assertEquals(0, built.getVersion()); - - assertNull(built.getCreationTime()); - assertNull(built.getModificationTime()); - assertNull(built.getId()); - } - - @Test - public void getName() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - String expectedName = constructName(FIRST_NAME, LAST_NAME); - assertEquals(expectedName, built.getName()); - } - - private String constructName(String firstName, String lastName) { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - @Test - public void prePersist() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertEquals(creationTime, modificationTime); - } - - @Test - public void preUpdate() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //Back to work - } - - built.preUpdate(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertTrue(modificationTime.after(creationTime)); - } - - @Test - public void update() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.update(FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - assertEquals(FIRST_NAME_UPDATED, built.getFirstName()); - assertEquals(LAST_NAME_UPDATED, built.getLastName()); - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java deleted file mode 100644 index 6575587..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; - -/** - * An utility class which contains useful methods for unit testing person related - * functions. - * @author Petri Kainulainen - */ -public class PersonTestUtil { - - public static PersonDTO createDTO(Long id, String firstName, String lastName) { - PersonDTO dto = new PersonDTO(); - - dto.setId(id); - dto.setFirstName(firstName); - dto.setLastName(lastName); - - return dto; - } - - public static Person createModelObject(Long id, String firstName, String lastName) { - Person model = Person.getBuilder(firstName, lastName).build(); - - model.setId(id); - - return model; - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecificationsTest.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecificationsTest.java deleted file mode 100644 index 516bac0..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonSpecificationsTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.Person_; -import org.junit.Before; -import org.junit.Test; -import org.springframework.data.jpa.domain.Specification; - -import javax.persistence.criteria.*; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonSpecificationsTest { - - private static final String SEARCH_TERM = "Foo"; - private static final String SEARCH_TERM_LIKE_PATTERN = "foo%"; - - private CriteriaBuilder criteriaBuilderMock; - - private CriteriaQuery criteriaQueryMock; - - private Root personRootMock; - - @Before - public void setUp() { - criteriaBuilderMock = mock(CriteriaBuilder.class); - criteriaQueryMock = mock(CriteriaQuery.class); - personRootMock = mock(Root.class); - } - - @Test - public void lastNameIsLike() { - Path lastNamePathMock = mock(Path.class); - when(personRootMock.get(Person_.lastName)).thenReturn(lastNamePathMock); - - Expression lastNameToLowerExpressionMock = mock(Expression.class); - when(criteriaBuilderMock.lower(lastNamePathMock)).thenReturn(lastNameToLowerExpressionMock); - - Predicate lastNameIsLikePredicateMock = mock(Predicate.class); - when(criteriaBuilderMock.like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN)).thenReturn(lastNameIsLikePredicateMock); - - Specification actual = PersonSpecifications.lastNameIsLike(SEARCH_TERM); - Predicate actualPredicate = actual.toPredicate(personRootMock, criteriaQueryMock, criteriaBuilderMock); - - verify(personRootMock, times(1)).get(Person_.lastName); - verifyNoMoreInteractions(personRootMock); - - verify(criteriaBuilderMock, times(1)).lower(lastNamePathMock); - verify(criteriaBuilderMock, times(1)).like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN); - verifyNoMoreInteractions(criteriaBuilderMock); - - verifyZeroInteractions(criteriaQueryMock, lastNamePathMock, lastNameIsLikePredicateMock); - - assertEquals(lastNameIsLikePredicateMock, actualPredicate); - } -} diff --git a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java b/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java deleted file mode 100644 index 6f0f49d..0000000 --- a/tutorial-part-four/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.data.jpa.domain.Specification; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class RepositoryPersonServiceTest { - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - private static final String SEARCH_TERM = "foo"; - - private RepositoryPersonService personService; - - private PersonRepository personRepositoryMock; - - @Before - public void setUp() { - personService = new RepositoryPersonService(); - - personRepositoryMock = mock(PersonRepository.class); - personService.setPersonRepository(personRepositoryMock); - } - - @Test - public void create() { - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, LAST_NAME); - Person persisted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.save(any(Person.class))).thenReturn(persisted); - - Person returned = personService.create(created); - - ArgumentCaptor personArgument = ArgumentCaptor.forClass(Person.class); - verify(personRepositoryMock, times(1)).save(personArgument.capture()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(created, personArgument.getValue()); - assertEquals(persisted, returned); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(deleted); - - Person returned = personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verify(personRepositoryMock, times(1)).delete(deleted); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(deleted, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(null); - - personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - } - - @Test - public void findAll() { - List persons = new ArrayList(); - when(personRepositoryMock.findAll()).thenReturn(persons); - - List returned = personService.findAll(); - - verify(personRepositoryMock, times(1)).findAll(); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(persons, returned); - } - - @Test - public void findById() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(person); - - Person returned = personService.findById(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(person, returned); - } - - @Test - public void search() { - List expected = new ArrayList(); - when(personRepositoryMock.findAll(any(Specification.class))).thenReturn(expected); - - List actual = personService.search(SEARCH_TERM); - - verify(personRepositoryMock, times(1)).findAll(any(Specification.class)); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(expected, actual); - } - - @Test - public void update() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(person); - - Person returned = personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(updated, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void updateWhenPersonIsNotFound() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(null); - - personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - } - - private void assertPerson(PersonDTO expected, Person actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getFirstName(), actual.getFirstName()); - assertEquals(expected.getLastName(), expected.getLastName()); - } - -} diff --git a/tutorial-part-one/README b/tutorial-part-one/README deleted file mode 100644 index 6ec546b..0000000 --- a/tutorial-part-one/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Part One: Configuration - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-one-configuration/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-one/pom.xml b/tutorial-part-one/pom.xml deleted file mode 100644 index 4eecf63..0000000 --- a/tutorial-part-one/pom.xml +++ /dev/null @@ -1,215 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-one - war - 0.1 - Spring Data JPA Tutorial Part One - Spring Data JPA Tutorial Part One - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - - data-jpa-tutorial-part-one - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - diff --git a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index a09a3ac..0000000 --- a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,115 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller"}) -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/controller/HomeController.java b/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/controller/HomeController.java deleted file mode 100644 index 3dbe88d..0000000 --- a/tutorial-part-one/src/main/java/net/petrikainulainen/spring/datajpa/controller/HomeController.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; - -/** - * @author Petri Kainulainen - */ -@Controller -public class HomeController { - - protected static final String HOME_VIEW = "home"; - - @RequestMapping(value = "/", method = RequestMethod.GET) - public String showPage() { - return HOME_VIEW; - } -} diff --git a/tutorial-part-one/src/main/resources/application.properties b/tutorial-part-one/src/main/resources/application.properties deleted file mode 100644 index c1823e6..0000000 --- a/tutorial-part-one/src/main/resources/application.properties +++ /dev/null @@ -1,28 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-one/src/main/resources/applicationContext.xml b/tutorial-part-one/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-one/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-one/src/main/resources/i18n/messages.properties b/tutorial-part-one/src/main/resources/i18n/messages.properties deleted file mode 100644 index bf4d466..0000000 --- a/tutorial-part-one/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,3 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part One -spring.data.jpa.example.homepage.title=Home -spring.data.jpa.example.welcome.message=Welcome to my Spring Data JPA Tutorial Example. If you can see this page, the example is working. \ No newline at end of file diff --git a/tutorial-part-one/src/main/resources/webdefault.xml b/tutorial-part-one/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-one/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-one/src/main/webapp/WEB-INF/jsp/home.jsp b/tutorial-part-one/src/main/webapp/WEB-INF/jsp/home.jsp deleted file mode 100644 index df4cc1b..0000000 --- a/tutorial-part-one/src/main/webapp/WEB-INF/jsp/home.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - -

-

- -

- - \ No newline at end of file diff --git a/tutorial-part-one/src/main/webapp/static/css/styles.css b/tutorial-part-one/src/main/webapp/static/css/styles.css deleted file mode 100644 index e519c04..0000000 --- a/tutorial-part-one/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - font-family: Verdana -} \ No newline at end of file diff --git a/tutorial-part-one/src/test/java/net/petrikainulainen/spring/datajpa/controller/HomeControllerTest.java b/tutorial-part-one/src/test/java/net/petrikainulainen/spring/datajpa/controller/HomeControllerTest.java deleted file mode 100644 index 8131603..0000000 --- a/tutorial-part-one/src/test/java/net/petrikainulainen/spring/datajpa/controller/HomeControllerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; - -/** - * @author Petri Kainulainen - */ -public class HomeControllerTest { - - private HomeController controller; - - @Before - public void setUp() { - controller = new HomeController(); - } - - @Test - public void showPage() { - String homeView = controller.showPage(); - assertEquals(HomeController.HOME_VIEW, homeView); - } -} diff --git a/tutorial-part-six/README b/tutorial-part-six/README deleted file mode 100644 index 38dd873..0000000 --- a/tutorial-part-six/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Six: Sorting - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-six-sorting/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-six/pom.xml b/tutorial-part-six/pom.xml deleted file mode 100644 index 4329f75..0000000 --- a/tutorial-part-six/pom.xml +++ /dev/null @@ -1,278 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-six - war - 0.1 - Spring Data JPA Tutorial Part Six - Spring Data JPA Tutorial Part Six - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - 2.3.2 - - - - - commons-lang - commons-lang - 2.6 - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - com.mysema.querydsl - querydsl-core - ${querydsl.version} - - - - com.mysema.querydsl - querydsl-apt - ${querydsl.version} - - - - com.mysema.querydsl - querydsl-jpa - ${querydsl.version} - - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - org.mockito - mockito-core - 1.8.5 - test - - - org.springframework - spring-test - ${spring.version} - test - - - - data-jpa-tutorial-part-two - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - com.mysema.maven - maven-apt-plugin - 1.0.2 - - - generate-sources - - process - - - - target/generated-sources - - com.mysema.query.apt.jpa.JPAAnnotationProcessor - - - - - - - diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index 7eed429..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * - * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller", - "net.petrikainulainen.spring.datajpa.service"}) -@EnableTransactionManagement -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java deleted file mode 100644 index 83ed0b6..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import java.util.Locale; - -/** - * An abstract controller class which provides utility methods useful - * to actual controller classes. - * @author Petri Kainulainen - */ -public abstract class AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractController.class); - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - @Resource - private MessageSource messageSource; - - /** - * Adds a new error message - * @param model A model which stores the the error message. - * @param code A message code which is used to fetch the correct message from the message source. - * @param params The parameters attached to the actual error message. - */ - protected void addErrorMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("adding error message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedErrorMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedErrorMessage); - model.addFlashAttribute(FLASH_ERROR_MESSAGE, localizedErrorMessage); - } - - /** - * Adds a new feedback message. - * @param model A model which stores the feedback message. - * @param code A message code which is used to fetch the actual message from the message source. - * @param params The parameters which are attached to the actual feedback message. - */ - protected void addFeedbackMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("Adding feedback message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedFeedbackMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedFeedbackMessage); - model.addFlashAttribute(FLASH_FEEDBACK_MESSAGE, localizedFeedbackMessage); - } - - /** - * Creates a redirect view path for a specific controller action - * @param path The path processed by the controller method. - * @return A redirect view path to the given controller method. - */ - protected String createRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * This method should only be used by unit tests. - * @param messageSource - */ - protected void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java deleted file mode 100644 index b3de94b..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java +++ /dev/null @@ -1,209 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.util.List; - -/** - * @author Petri Kainulainen - */ -@Controller -@SessionAttributes("person") -public class PersonController extends AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); - - protected static final String ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND = "error.message.deleted.not.found"; - protected static final String ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND = "error.message.edited.not.found"; - - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_CREATED = "feedback.message.person.created"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_DELETED = "feedback.message.person.deleted"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_EDITED = "feedback.message.person.edited"; - - protected static final String MODEL_ATTIRUTE_PERSON = "person"; - protected static final String MODEL_ATTRIBUTE_PERSONS = "persons"; - protected static final String MODEL_ATTRIBUTE_SEARCH_CRITERIA = "searchCriteria"; - - protected static final String PERSON_ADD_FORM_VIEW = "person/create"; - protected static final String PERSON_EDIT_FORM_VIEW = "person/edit"; - protected static final String PERSON_LIST_VIEW = "person/list"; - protected static final String PERSON_SEARCH_RESULT_VIEW = "person/searchResults"; - - protected static final String REQUEST_MAPPING_LIST = "/"; - - @Resource - private PersonService personService; - - /** - * Processes delete person requests. - * @param id The id of the deleted person. - * @param attributes - * @return - */ - @RequestMapping(value = "/person/delete/{id}", method = RequestMethod.GET) - public String delete(@PathVariable("id") Long id, RedirectAttributes attributes) { - LOGGER.debug("Deleting person with id: " + id); - - try { - Person deleted = personService.delete(id); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_DELETED, deleted.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes search person requests. - * @param searchCriteria The search criteria. - * @param model - * @return - */ - @RequestMapping(value = "/person/search", method = RequestMethod.POST) - public String search(@ModelAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA)SearchDTO searchCriteria, Model model) { - LOGGER.debug("Searching persons with search criteria: " + searchCriteria); - - String searchTerm = searchCriteria.getSearchTerm(); - List persons = personService.search(searchTerm); - LOGGER.debug("Found " + persons.size() + " persons"); - - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - - return PERSON_SEARCH_RESULT_VIEW; - } - - /** - * Processes create person requests. - * @param model - * @return The name of the create person form view. - */ - @RequestMapping(value = "/person/create", method = RequestMethod.GET) - public String showCreatePersonForm(Model model) { - LOGGER.debug("Rendering create person form"); - - model.addAttribute(MODEL_ATTIRUTE_PERSON, new PersonDTO()); - - return PERSON_ADD_FORM_VIEW; - } - - /** - * Processes the submissions of create person form. - * @param created The information of the created persons. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/create", method = RequestMethod.POST) - public String submitCreatePersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO created, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Create person form was submitted with information: " + created); - - if (bindingResult.hasErrors()) { - return PERSON_ADD_FORM_VIEW; - } - - Person person = personService.create(created); - - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_CREATED, person.getName()); - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes edit person requests. - * @param id The id of the edited person. - * @param model - * @param attributes - * @return The name of the edit person form view. - */ - @RequestMapping(value = "/person/edit/{id}", method = RequestMethod.GET) - public String showEditPersonForm(@PathVariable("id") Long id, Model model, RedirectAttributes attributes) { - LOGGER.debug("Rendering edit person form for person with id: " + id); - - Person person = personService.findById(id); - if (person == null) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - model.addAttribute(MODEL_ATTIRUTE_PERSON, constructFormObject(person)); - - return PERSON_EDIT_FORM_VIEW; - } - - /** - * Processes the submissions of edit person form. - * @param updated The information of the edited person. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/edit", method = RequestMethod.POST) - public String submitEditPersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO updated, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Edit person form was submitted with information: " + updated); - - if (bindingResult.hasErrors()) { - LOGGER.debug("Edit person form contains validation errors. Rendering form view."); - return PERSON_EDIT_FORM_VIEW; - } - - try { - Person person = personService.update(updated); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_EDITED, person.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person was found with id: " + updated.getId()); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - private PersonDTO constructFormObject(Person person) { - PersonDTO formObject = new PersonDTO(); - - formObject.setId(person.getId()); - formObject.setFirstName(person.getFirstName()); - formObject.setLastName(person.getLastName()); - - return formObject; - } - - /** - * Processes requests to home page which lists all available persons. - * @param model - * @return The name of the person list view. - */ - @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) - public String showList(Model model) { - LOGGER.debug("Rendering person list page"); - - List persons = personService.findAll(); - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - model.addAttribute(MODEL_ATTRIBUTE_SEARCH_CRITERIA, new SearchDTO()); - - return PERSON_LIST_VIEW; - } - - /** - * This setter method should only be used by unit tests - * @param personService - */ - protected void setPersonService(PersonService personService) { - this.personService = personService; - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java deleted file mode 100644 index 881ddb6..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.hibernate.validator.constraints.NotEmpty; - - -/** - * A DTO object which is used as a form object - * in create person and edit person forms. - * @author Petri Kainulainen - */ -public class PersonDTO { - - private Long id; - - @NotEmpty - private String firstName; - - @NotEmpty - private String lastName; - - public PersonDTO() { - - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - 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; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java deleted file mode 100644 index 84c6cfb..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; - -/** - * @author Petri Kainulainen - */ -public class SearchDTO { - - private String searchTerm; - - public SearchDTO() { - - } - - public String getSearchTerm() { - return searchTerm; - } - - public void setSearchTerm(String searchTerm) { - this.searchTerm = searchTerm; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java deleted file mode 100644 index d59fcc3..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import javax.persistence.*; -import java.util.Date; - -/** - * An entity class which contains the information of a single person. - * @author Petri Kainulainen - */ -@Entity -@NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(?1)") -@Table(name = "persons") -public class Person { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "creation_time", nullable = false) - private Date creationTime; - - @Column(name = "first_name", nullable = false) - private String firstName; - - @Column(name = "last_name", nullable = false) - private String lastName; - - @Column(name = "modification_time", nullable = false) - private Date modificationTime; - - @Version - private long version = 0; - - public Long getId() { - return id; - } - - /** - * Gets a builder which is used to create Person objects. - * @param firstName The first name of the created user. - * @param lastName The last name of the created user. - * @return A new Builder instance. - */ - public static Builder getBuilder(String firstName, String lastName) { - return new Builder(firstName, lastName); - } - - public Date getCreationTime() { - return creationTime; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - /** - * Gets the full name of the person. - * @return The full name of the person. - */ - @Transient - public String getName() { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - public Date getModificationTime() { - return modificationTime; - } - - public long getVersion() { - return version; - } - - public void update(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - @PreUpdate - public void preUpdate() { - modificationTime = new Date(); - } - - @PrePersist - public void prePersist() { - Date now = new Date(); - creationTime = now; - modificationTime = now; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * A Builder class used to create new Person objects. - */ - public static class Builder { - Person built; - - /** - * Creates a new Builder instance. - * @param firstName The first name of the created Person object. - * @param lastName The last name of the created Person object. - */ - Builder(String firstName, String lastName) { - built = new Person(); - built.firstName = firstName; - built.lastName = lastName; - } - - /** - * Builds the new Person object. - * @return The created Person object. - */ - public Person build() { - return built; - } - } - - /** - * This setter method should only be used by unit tests. - * @param id - */ - protected void setId(Long id) { - this.id = id; - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java deleted file mode 100644 index bf9b7be..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicates.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import com.mysema.query.types.Predicate; -import net.petrikainulainen.spring.datajpa.model.QPerson; - -/** - * A class which is used to create Querydsl predicates. - * @author Petri Kainulainen - */ -public class PersonPredicates { - - public static Predicate lastNameIsLike(final String searchTerm) { - QPerson person = QPerson.person; - return person.lastName.startsWithIgnoreCase(searchTerm); - } -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java deleted file mode 100644 index 43b1bb8..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.querydsl.QueryDslPredicateExecutor; - -import java.util.List; - -/** - * Specifies methods used to obtain and modify person related information - * which is stored in the database. - * @author Petri Kainulainen - */ -public interface PersonRepository extends JpaRepository, QueryDslPredicateExecutor { - -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java deleted file mode 100644 index 35cbd2e..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -/** - * This exception is thrown if the wanted person is not found. - * @author Petri Kainulainen - */ -public class PersonNotFoundException extends Exception { -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java deleted file mode 100644 index b431b31..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; - -import java.util.List; - -/** - * Declares methods used to obtain and modify person information. - * @author Petri Kainulainen - */ -public interface PersonService { - - /** - * Creates a new person. - * @param created The information of the created person. - * @return The created person. - */ - public Person create(PersonDTO created); - - /** - * Deletes a person. - * @param personId The id of the deleted person. - * @return The deleted person. - * @throws PersonNotFoundException if no person is found with the given id. - */ - public Person delete(Long personId) throws PersonNotFoundException; - - /** - * Finds all persons. - * @return A list of persons. - */ - public List findAll(); - - /** - * Finds person by id. - * @param id The id of the wanted person. - * @return The found person. If no person is found, this method returns null. - */ - public Person findById(Long id); - - /** - * Searches persons by using the given search term as a parameter. - * @param searchTerm - * @return A list of persons whose last name begins with the given search term. If no persons is found, this method - * returns an empty list. This search is case insensitive. - */ - public List search(String searchTerm); - - /** - * Updates the information of a person. - * @param updated The information of the updated person. - * @return The updated person. - * @throws PersonNotFoundException if no person is found with given id. - */ - public Person update(PersonDTO updated) throws PersonNotFoundException; -} diff --git a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java b/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java deleted file mode 100644 index 0db3827..0000000 --- a/tutorial-part-six/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java +++ /dev/null @@ -1,133 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import com.mysema.query.types.OrderSpecifier; -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.QPerson; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.List; - -import static net.petrikainulainen.spring.datajpa.repository.PersonPredicates.lastNameIsLike; - -/** - * This implementation of the PersonService interface communicates with - * the database by using a Spring Data JPA repository. - * @author Petri Kainulainen - */ -@Service -public class RepositoryPersonService implements PersonService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class); - - @Resource - private PersonRepository personRepository; - - @Transactional - @Override - public Person create(PersonDTO created) { - LOGGER.debug("Creating a new person with information: " + created); - - Person person = Person.getBuilder(created.getFirstName(), created.getLastName()).build(); - - return personRepository.save(person); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person delete(Long personId) throws PersonNotFoundException { - LOGGER.debug("Deleting person with id: " + personId); - - Person deleted = personRepository.findOne(personId); - - if (deleted == null) { - LOGGER.debug("No person found with id: " + personId); - throw new PersonNotFoundException(); - } - - personRepository.delete(deleted); - return deleted; - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - LOGGER.debug("Finding all persons"); - //Passes the Sort object to the repository. - return personRepository.findAll(sortByLastNameAsc()); - } - - /** - * Returns a Sort object which sorts persons in ascending order by using the last name. - * @return - */ - private Sort sortByLastNameAsc() { - return new Sort(Sort.Direction.ASC, "lastName"); - } - - @Transactional(readOnly = true) - @Override - public Person findById(Long id) { - LOGGER.debug("Finding person by id: " + id); - return personRepository.findOne(id); - } - - @Transactional(readOnly = true) - @Override - public List search(String searchTerm) { - LOGGER.debug("Searching persons with search term: " + searchTerm); - - //Passes the specification created by PersonPredicates class and the OrderSpecifier object to the repository. - Iterable persons = personRepository.findAll(lastNameIsLike(searchTerm), orderByLastNameAsc()); - - return constructList(persons); - } - - /** - * Returns an OrderSpecifier object which sorts person in ascending order by using the last name. - * @return - */ - private OrderSpecifier orderByLastNameAsc() { - return QPerson.person.lastName.asc(); - } - - private List constructList(Iterable persons) { - List list = new ArrayList(); - for (Person person: persons) { - list.add(person); - } - return list; - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person update(PersonDTO updated) throws PersonNotFoundException { - LOGGER.debug("Updating person with information: " + updated); - - Person person = personRepository.findOne(updated.getId()); - - if (person == null) { - LOGGER.debug("No person found with id: " + updated.getId()); - throw new PersonNotFoundException(); - } - - person.update(updated.getFirstName(), updated.getLastName()); - - return person; - } - - /** - * This setter method should be used only by unit tests. - * @param personRepository - */ - protected void setPersonRepository(PersonRepository personRepository) { - this.personRepository = personRepository; - } -} diff --git a/tutorial-part-six/src/main/resources/application.properties b/tutorial-part-six/src/main/resources/application.properties deleted file mode 100644 index 426c303..0000000 --- a/tutorial-part-six/src/main/resources/application.properties +++ /dev/null @@ -1,29 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.hbm2ddl.auto=create-drop -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-six/src/main/resources/applicationContext.xml b/tutorial-part-six/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-six/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-six/src/main/resources/i18n/messages.properties b/tutorial-part-six/src/main/resources/i18n/messages.properties deleted file mode 100644 index d89d992..0000000 --- a/tutorial-part-six/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,44 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part Two - - -person.list.link.label=View persons - -#Person list page -person.list.page.title=Persons -person.list.page.label.no.persons.found=No persons was found. -person.create.link.label=Create person -person.edit.link.label=Edit person -person.delete.link.label=Delete person -person.search.form.title=Search -person.search.form.submit.label=Search -person.search.searchterm.label=Search Term -person.search.result.page.title=Search Results for Search Term - -SearchType.METHOD_NAME=Method Name -SearchType.NAMED_QUERY=Named Query -SearchType.QUERY_ANNOTATION=Query Annotation - -#Create person page -person.create.page.title=Create Person -person.create.page.submit.label=Create - -#Edit person page -person.edit.page.title=Edit Person -person.edit.page.submit.label=Edit - -#General person labels -person.label.firstName=First name -person.label.lastName=Last name - -#Error messages -error.message.deleted.not.found=Deleted person was not found. -error.message.edited.not.found=Edited person was not found. - -#Feedback messages -feedback.message.person.created=Person with name {0} was created. -feedback.message.person.deleted=Person with name {0} was deleted. -feedback.message.person.edited=Person with name {0} was edited. - -#Validation error messages -NotEmpty.person.firstName=Enter first name -NotEmpty.person.lastName=Enter last name \ No newline at end of file diff --git a/tutorial-part-six/src/main/resources/webdefault.xml b/tutorial-part-six/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-six/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/create.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/create.jsp deleted file mode 100644 index 508cf83..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/create.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/edit.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/edit.jsp deleted file mode 100644 index c9d5b53..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/edit.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- - -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/list.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/list.jsp deleted file mode 100644 index df816d3..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/list.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -
- -
-
- -
-
-
- - - - \ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/navigation.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/navigation.jsp deleted file mode 100644 index b2ea406..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/navigation.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -
- | - -
diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/personList.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/personList.jsp deleted file mode 100644 index d9d5096..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/personList.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -

- - - - - - - - - - - - - - - - - - -
">">
-
- -

- -

-
\ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp deleted file mode 100644 index eb8ec7d..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> - -
- -
- - -
-
- "/> -
-
-
\ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp b/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp deleted file mode 100644 index b47b78d..0000000 --- a/tutorial-part-six/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - - -

:

- - - \ No newline at end of file diff --git a/tutorial-part-six/src/main/webapp/static/css/styles.css b/tutorial-part-six/src/main/webapp/static/css/styles.css deleted file mode 100644 index 5ac2da3..0000000 --- a/tutorial-part-six/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,31 +0,0 @@ -body { - font-family: Verdana -} - -.error { - color: #ff0000; -} - -.messageblock { - color: #000; - background-color: #cbf7c8; - border: 3px solid #3bdb2a; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} - -.errorblock { - color: #000; - background-color: #ffEEEE; - border: 3px solid #ff0000; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} \ No newline at end of file diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java deleted file mode 100644 index cfce15c..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.petrikainulainen.spring.datajpa.context; - - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * A test context which is used for unit testing controllers. - * @author Petri Kainulainen - */ -@Configuration -public class TestContext { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} \ No newline at end of file diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java deleted file mode 100644 index 2ca7fd7..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.Locale; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class AbstractControllerTest { - - private static final String ERROR_MESSAGE = "errorMessage"; - private static final String ERROR_MESSAGE_CODE = "errorMessageCode"; - private static final String FEEDBACK_MESSAGE = "feedbackMessage"; - private static final String FEEDBACK_MESSAGE_CODE = "feedbackMessageCode"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String REDIRECT_PATH = "/foo"; - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private TestController controller; - - private MessageSource messageSourceMock; - - @Before - public void setUp() { - controller = new TestController(); - - messageSourceMock = mock(MessageSource.class); - controller.setMessageSource(messageSourceMock); - } - - @Test - public void addErrorMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(ERROR_MESSAGE); - - controller.addErrorMessage(model, ERROR_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String errorMessage = (String) model.getFlashAttributes().get(FLASH_ERROR_MESSAGE); - assertEquals(ERROR_MESSAGE, errorMessage); - } - - @Test - public void addFeedbackMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - - controller.addFeedbackMessage(model, FEEDBACK_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String feedbackMessage = (String) model.getFlashAttributes().get(FLASH_FEEDBACK_MESSAGE); - assertEquals(FEEDBACK_MESSAGE, feedbackMessage); - } - - @Test - public void createRedirectViewPath() { - String redirectView = controller.createRedirectViewPath(REDIRECT_PATH); - String expectedView = buildExpectedRedirectViewPath(REDIRECT_PATH); - - verifyZeroInteractions(messageSourceMock); - assertEquals(expectedView, redirectView); - } - - private String buildExpectedRedirectViewPath(String redirectPath) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(redirectPath); - return builder.toString(); - } - - - private class TestController extends AbstractController { - - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java deleted file mode 100644 index e83178c..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.context.TestContext; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.context.MessageSource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.when; - -/** - * An abstract base class for all controller unit tests. - * @author Petri Kainulainen - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {TestContext.class}) -public abstract class AbstractTestController { - - protected static final String ERROR_MESSAGE = "errorMessage"; - protected static final String FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private MessageSource messageSourceMock; - - @Resource - private Validator validator; - - @Before - public void setUp() { - messageSourceMock = mock(MessageSource.class); - setUpTest(); - } - - protected abstract void setUpTest(); - - /** - * Asserts that an error message is present. - * @param model The model which is used to store the error message. - * @param messageCode The message code of the expected error message. - */ - protected void assertErrorMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_ERROR_MESSAGE); - } - - /** - * Asserts that a feedback message is present. - * @param model The model which is used to store the feedback message. - * @param messageCode - */ - protected void assertFeedbackMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_FEEDBACK_MESSAGE); - } - - private void assertFlashMessages(RedirectAttributes model, String messageCode, String flashMessageParameterName) { - Map flashMessages = model.getFlashAttributes(); - Object message = flashMessages.get(flashMessageParameterName); - assertNotNull(message); - flashMessages.remove(message); - assertTrue(flashMessages.isEmpty()); - - verify(messageSourceMock, times(1)).getMessage(eq(messageCode), any(Object[].class), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - } - - /** - * Asserts that the binding result contains specified field errors. - * @param result The binding result - * @param fieldNames The names which should have validation errors. - */ - protected void assertFieldErrors(BindingResult result, String... fieldNames) { - assertEquals(fieldNames.length, result.getFieldErrorCount()); - for (String fieldName : fieldNames) { - assertNotNull(result.getFieldError(fieldName)); - } - } - - /** - * Binds and validates the given form object. - * @param request The http servlet request object. - * @param formObject A form object. - * @return A binding result containing the outcome of binding and validation. - */ - protected BindingResult bindAndValidate(HttpServletRequest request, Object formObject) { - WebDataBinder binder = new WebDataBinder(formObject); - binder.setValidator(validator); - binder.bind(new MutablePropertyValues(request.getParameterMap())); - binder.getValidator().validate(binder.getTarget(), binder.getBindingResult()); - return binder.getBindingResult(); - } - - /** - * Creates an expected redirect view path. - * @param path The path to the requested view. - * @return The expected redirect view path. - */ - protected String createExpectedRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * Initializes the message source mock to return an error message when - * the error message code given as a a parameter is used to get message - * from message source. - * @param errorMessageCode The wanted error message code. - */ - protected void initMessageSourceForErrorMessage(String errorMessageCode) { - when(messageSourceMock.getMessage(eq(errorMessageCode), any(Object[].class), any(Locale.class))).thenReturn(ERROR_MESSAGE); - } - - /** - * Initializes the message source mock to return a feedback message when - * the feedback message code given as a parameter is used to get message - * from message source. - * @param feedbackMessageCode The wanted feedback message code. - */ - protected void initMessageSourceForFeedbackMessage(String feedbackMessageCode) { - when(messageSourceMock.getMessage(eq(feedbackMessageCode), any(Object[].class), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - } - - /** - * Returns the message source mock. - * @return - */ - protected MessageSource getMessageSourceMock() { - return messageSourceMock; - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java deleted file mode 100644 index c05bfa5..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java +++ /dev/null @@ -1,365 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.*; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonControllerTest extends AbstractTestController { - - private static final String FIELD_NAME_FIRST_NAME = "firstName"; - private static final String FIELD_NAME_LAST_NAME = "lastName"; - - private static final Long PERSON_ID = Long.valueOf(5); - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private static final String SEARCH_TERM = "foo"; - - private PersonController controller; - - private PersonService personServiceMock; - - @Override - public void setUpTest() { - controller = new PersonController(); - - controller.setMessageSource(getMessageSourceMock()); - - personServiceMock = mock(PersonService.class); - controller.setPersonService(personServiceMock); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.delete(PERSON_ID)).thenReturn(deleted); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personServiceMock.delete(PERSON_ID)).thenThrow(new PersonNotFoundException()); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void search() { - SearchDTO searchCriteria = createSearchDTO(); - List expected = new ArrayList(); - when(personServiceMock.search(searchCriteria.getSearchTerm())).thenReturn(expected); - - BindingAwareModelMap model = new BindingAwareModelMap(); - String view = controller.search(searchCriteria, model); - - verify(personServiceMock, times(1)).search(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_SEARCH_RESULT_VIEW, view); - List actual = (List) model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS); - assertEquals(expected, actual); - } - - private SearchDTO createSearchDTO() { - SearchDTO dto = new SearchDTO(); - dto.setSearchTerm(SEARCH_TERM); - return dto; - } - - @Test - public void showCreatePersonForm() { - Model model = new BindingAwareModelMap(); - - String view = controller.showCreatePersonForm(model); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - - PersonDTO added = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - assertNotNull(added); - - assertNull(added.getId()); - assertNull(added.getFirstName()); - assertNull(added.getLastName()); - } - - @Test - public void submitCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME, LAST_NAME); - Person model = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.create(created)).thenReturn(model); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - - String expectedViewPath = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedViewPath, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - } - - @Test - public void submitEmptyCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = new PersonDTO(); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyFirstName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, null, LAST_NAME); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyLastName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, null); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_LAST_NAME); - } - - @Test - public void showEditPersonForm() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.findById(PERSON_ID)).thenReturn(person); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - - PersonDTO formObject = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - - assertNotNull(formObject); - assertEquals(person.getId(), formObject.getId()); - assertEquals(person.getFirstName(), formObject.getFirstName()); - assertEquals(person.getLastName(), formObject.getLastName()); - } - - @Test - public void showEditPersonFormWhenPersonIsNotFound() { - when(personServiceMock.findById(PERSON_ID)).thenReturn(null); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEditPersonForm() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenReturn(person); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - assertEquals(updated.getFirstName(), person.getFirstName()); - assertEquals(updated.getLastName(), person.getLastName()); - } - - @Test - public void submitEditPersonFormWhenPersonIsNotFound() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenThrow(new PersonNotFoundException()); - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEmptyEditPersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitEditPersonFormWhenFirstNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, LAST_NAME_UPDATED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitEditPersonFormWhenLastNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_LAST_NAME); - } - - @Test - public void showList() { - List persons = new ArrayList(); - when(personServiceMock.findAll()).thenReturn(persons); - - Model model = new BindingAwareModelMap(); - String view = controller.showList(model); - - verify(personServiceMock, times(1)).findAll(); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_LIST_VIEW, view); - assertEquals(persons, model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS)); - - SearchDTO searchCriteria = (SearchDTO) model.asMap().get(PersonController.MODEL_ATTRIBUTE_SEARCH_CRITERIA); - assertNotNull(searchCriteria); - assertNull(searchCriteria.getSearchTerm()); - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java deleted file mode 100644 index de7a368..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.junit.Test; - -import java.util.Date; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Petri Kainulainen - */ -public class PersonTest { - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "Foo1"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "Bar1"; - - @Test - public void build() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - assertEquals(FIRST_NAME, built.getFirstName()); - assertEquals(LAST_NAME, built.getLastName()); - assertEquals(0, built.getVersion()); - - assertNull(built.getCreationTime()); - assertNull(built.getModificationTime()); - assertNull(built.getId()); - } - - @Test - public void getName() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - String expectedName = constructName(FIRST_NAME, LAST_NAME); - assertEquals(expectedName, built.getName()); - } - - private String constructName(String firstName, String lastName) { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - @Test - public void prePersist() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertEquals(creationTime, modificationTime); - } - - @Test - public void preUpdate() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //Back to work - } - - built.preUpdate(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertTrue(modificationTime.after(creationTime)); - } - - @Test - public void update() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.update(FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - assertEquals(FIRST_NAME_UPDATED, built.getFirstName()); - assertEquals(LAST_NAME_UPDATED, built.getLastName()); - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java deleted file mode 100644 index 6575587..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; - -/** - * An utility class which contains useful methods for unit testing person related - * functions. - * @author Petri Kainulainen - */ -public class PersonTestUtil { - - public static PersonDTO createDTO(Long id, String firstName, String lastName) { - PersonDTO dto = new PersonDTO(); - - dto.setId(id); - dto.setFirstName(firstName); - dto.setLastName(lastName); - - return dto; - } - - public static Person createModelObject(Long id, String firstName, String lastName) { - Person model = Person.getBuilder(firstName, lastName).build(); - - model.setId(id); - - return model; - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java deleted file mode 100644 index e01d371..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/repository/PersonPredicatesTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import com.mysema.query.types.Predicate; -import com.mysema.query.types.expr.BooleanExpression; -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; - -/** - * @author Petri Kainulainen - */ -public class PersonPredicatesTest { - - private static final String SEARCH_TERM = "Foo"; - private static final String EXPECTED_PREDICATE_STRING = "startsWithIgnoreCase(person.lastName,Foo)"; - - @Test - public void lastNameLike() { - Predicate predicate = PersonPredicates.lastNameIsLike(SEARCH_TERM); - String predicateAsString = predicate.toString(); - assertEquals(EXPECTED_PREDICATE_STRING, predicateAsString); - } -} diff --git a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java b/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java deleted file mode 100644 index 8d9a5b9..0000000 --- a/tutorial-part-six/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import com.mysema.query.types.Order; -import com.mysema.query.types.OrderSpecifier; -import com.mysema.query.types.expr.BooleanExpression; -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.QPerson; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.data.domain.Sort; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class RepositoryPersonServiceTest { - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - private static final String SEARCH_TERM = "foo"; - - private RepositoryPersonService personService; - - private PersonRepository personRepositoryMock; - - @Before - public void setUp() { - personService = new RepositoryPersonService(); - - personRepositoryMock = mock(PersonRepository.class); - personService.setPersonRepository(personRepositoryMock); - } - - @Test - public void create() { - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, LAST_NAME); - Person persisted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.save(any(Person.class))).thenReturn(persisted); - - Person returned = personService.create(created); - - ArgumentCaptor personArgument = ArgumentCaptor.forClass(Person.class); - verify(personRepositoryMock, times(1)).save(personArgument.capture()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(created, personArgument.getValue()); - assertEquals(persisted, returned); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(deleted); - - Person returned = personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verify(personRepositoryMock, times(1)).delete(deleted); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(deleted, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(null); - - personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - } - - @Test - public void findAll() { - List persons = new ArrayList(); - when(personRepositoryMock.findAll(any(Sort.class))).thenReturn(persons); - - List returned = personService.findAll(); - - ArgumentCaptor sortArgument = ArgumentCaptor.forClass(Sort.class); - verify(personRepositoryMock, times(1)).findAll(sortArgument.capture()); - - verifyNoMoreInteractions(personRepositoryMock); - - Sort actualSort = sortArgument.getValue(); - assertEquals(Sort.Direction.ASC, actualSort.getOrderFor("lastName").getDirection()); - - assertEquals(persons, returned); - } - - @Test - public void findById() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(person); - - Person returned = personService.findById(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(person, returned); - } - - @Test - public void search() { - List expected = new ArrayList(); - when(personRepositoryMock.findAll(any(BooleanExpression.class), any(OrderSpecifier.class))).thenReturn(expected); - - List actual = personService.search(SEARCH_TERM); - - ArgumentCaptor orderArgument = ArgumentCaptor.forClass(OrderSpecifier.class); - verify(personRepositoryMock, times(1)).findAll(any(BooleanExpression.class), orderArgument.capture()); - - verifyNoMoreInteractions(personRepositoryMock); - - OrderSpecifier actualOrder = orderArgument.getValue(); - assertEquals(Order.ASC, actualOrder.getOrder()); - assertEquals(QPerson.person.lastName, actualOrder.getTarget()); - - assertEquals(expected, actual); - } - - @Test - public void update() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(person); - - Person returned = personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(updated, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void updateWhenPersonIsNotFound() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(null); - - personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - } - - private void assertPerson(PersonDTO expected, Person actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getFirstName(), actual.getFirstName()); - assertEquals(expected.getLastName(), expected.getLastName()); - } - -} diff --git a/tutorial-part-three/LICENSE b/tutorial-part-three/LICENSE deleted file mode 100644 index b333aa5..0000000 --- a/tutorial-part-three/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2011 Petri Kainulainen - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/tutorial-part-three/README b/tutorial-part-three/README deleted file mode 100644 index 364c88d..0000000 --- a/tutorial-part-three/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Three: Custom Queries with Query Methods - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-three-custom-queries-with-query-methods/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-three/pom.xml b/tutorial-part-three/pom.xml deleted file mode 100644 index 9cd033e..0000000 --- a/tutorial-part-three/pom.xml +++ /dev/null @@ -1,239 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-three - war - 0.1 - Spring Data JPA Tutorial Part Three - Spring Data JPA Tutorial Part Three - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - - - - - commons-lang - commons-lang - 2.6 - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - org.mockito - mockito-core - 1.8.5 - test - - - org.springframework - spring-test - ${spring.version} - test - - - - data-jpa-tutorial-part-two - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index 7eed429..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * - * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller", - "net.petrikainulainen.spring.datajpa.service"}) -@EnableTransactionManagement -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java deleted file mode 100644 index 83ed0b6..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import java.util.Locale; - -/** - * An abstract controller class which provides utility methods useful - * to actual controller classes. - * @author Petri Kainulainen - */ -public abstract class AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractController.class); - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - @Resource - private MessageSource messageSource; - - /** - * Adds a new error message - * @param model A model which stores the the error message. - * @param code A message code which is used to fetch the correct message from the message source. - * @param params The parameters attached to the actual error message. - */ - protected void addErrorMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("adding error message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedErrorMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedErrorMessage); - model.addFlashAttribute(FLASH_ERROR_MESSAGE, localizedErrorMessage); - } - - /** - * Adds a new feedback message. - * @param model A model which stores the feedback message. - * @param code A message code which is used to fetch the actual message from the message source. - * @param params The parameters which are attached to the actual feedback message. - */ - protected void addFeedbackMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("Adding feedback message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedFeedbackMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedFeedbackMessage); - model.addFlashAttribute(FLASH_FEEDBACK_MESSAGE, localizedFeedbackMessage); - } - - /** - * Creates a redirect view path for a specific controller action - * @param path The path processed by the controller method. - * @return A redirect view path to the given controller method. - */ - protected String createRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * This method should only be used by unit tests. - * @param messageSource - */ - protected void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java deleted file mode 100644 index 72e57ec..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java +++ /dev/null @@ -1,202 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.util.List; - -/** - * @author Petri Kainulainen - */ -@Controller -@SessionAttributes("person") -public class PersonController extends AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); - - protected static final String ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND = "error.message.deleted.not.found"; - protected static final String ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND = "error.message.edited.not.found"; - - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_CREATED = "feedback.message.person.created"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_DELETED = "feedback.message.person.deleted"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_EDITED = "feedback.message.person.edited"; - - protected static final String MODEL_ATTIRUTE_PERSON = "person"; - protected static final String MODEL_ATTRIBUTE_PERSONS = "persons"; - protected static final String MODEL_ATTRIBUTE_SEARCHCRITERIA = "searchCriteria"; - - protected static final String PERSON_ADD_FORM_VIEW = "person/create"; - protected static final String PERSON_EDIT_FORM_VIEW = "person/edit"; - protected static final String PERSON_LIST_VIEW = "person/list"; - protected static final String PERSON_SEARCH_RESULT_VIEW = "person/searchResults"; - - protected static final String REQUEST_MAPPING_LIST = "/"; - - @Resource - private PersonService personService; - - /** - * Processes delete person requests. - * @param id The id of the deleted person. - * @param attributes - * @return - */ - @RequestMapping(value = "/person/delete/{id}", method = RequestMethod.GET) - public String delete(@PathVariable("id") Long id, RedirectAttributes attributes) { - LOGGER.debug("Deleting person with id: " + id); - - try { - Person deleted = personService.delete(id); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_DELETED, deleted.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - @RequestMapping(value = "/person/search", method = RequestMethod.POST) - public String search(@ModelAttribute(MODEL_ATTRIBUTE_SEARCHCRITERIA) SearchDTO searchCriteria, Model model) { - LOGGER.debug("Searching persons with search criteria: " + searchCriteria); - - List persons = personService.search(searchCriteria); - LOGGER.debug("Found " + persons.size() + " persons"); - - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - - return PERSON_SEARCH_RESULT_VIEW; - } - - /** - * Processes create person requests. - * @param model - * @return The name of the create person form view. - */ - @RequestMapping(value = "/person/create", method = RequestMethod.GET) - public String showCreatePersonForm(Model model) { - LOGGER.debug("Rendering create person form"); - - model.addAttribute(MODEL_ATTIRUTE_PERSON, new PersonDTO()); - - return PERSON_ADD_FORM_VIEW; - } - - /** - * Processes the submissions of create person form. - * @param created The information of the created persons. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/create", method = RequestMethod.POST) - public String submitCreatePersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO created, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Create person form was submitted with information: " + created); - - if (bindingResult.hasErrors()) { - return PERSON_ADD_FORM_VIEW; - } - - Person person = personService.create(created); - - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_CREATED, person.getName()); - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes edit person requests. - * @param id The id of the edited person. - * @param model - * @param attributes - * @return The name of the edit person form view. - */ - @RequestMapping(value = "/person/edit/{id}", method = RequestMethod.GET) - public String showEditPersonForm(@PathVariable("id") Long id, Model model, RedirectAttributes attributes) { - LOGGER.debug("Rendering edit person form for person with id: " + id); - - Person person = personService.findById(id); - if (person == null) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - model.addAttribute(MODEL_ATTIRUTE_PERSON, constructFormObject(person)); - - return PERSON_EDIT_FORM_VIEW; - } - - /** - * Processes the submissions of edit person form. - * @param updated The information of the edited person. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/edit", method = RequestMethod.POST) - public String submitEditPersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO updated, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Edit person form was submitted with information: " + updated); - - if (bindingResult.hasErrors()) { - LOGGER.debug("Edit person form contains validation errors. Rendering form view."); - return PERSON_EDIT_FORM_VIEW; - } - - try { - Person person = personService.update(updated); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_EDITED, person.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person was found with id: " + updated.getId()); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - private PersonDTO constructFormObject(Person person) { - PersonDTO formObject = new PersonDTO(); - - formObject.setId(person.getId()); - formObject.setFirstName(person.getFirstName()); - formObject.setLastName(person.getLastName()); - - return formObject; - } - - /** - * Processes requests to home page which lists all available persons. - * @param model - * @return The name of the person list view. - */ - @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) - public String showList(Model model) { - LOGGER.debug("Rendering person list page"); - - List persons = personService.findAll(); - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - model.addAttribute(MODEL_ATTRIBUTE_SEARCHCRITERIA, new SearchDTO()); - - return PERSON_LIST_VIEW; - } - - /** - * This setter method should only be used by unit tests - * @param personService - */ - protected void setPersonService(PersonService personService) { - this.personService = personService; - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java deleted file mode 100644 index 881ddb6..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.hibernate.validator.constraints.NotEmpty; - - -/** - * A DTO object which is used as a form object - * in create person and edit person forms. - * @author Petri Kainulainen - */ -public class PersonDTO { - - private Long id; - - @NotEmpty - private String firstName; - - @NotEmpty - private String lastName; - - public PersonDTO() { - - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - 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; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java deleted file mode 100644 index bbeb036..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchDTO.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; - -/** - * A DTO class which is used as a form object in the search form. - * @author Petri Kainulainen - */ -public class SearchDTO { - - private String searchTerm; - - private SearchType searchType; - - public SearchDTO() { - - } - - public String getSearchTerm() { - return searchTerm; - } - - public void setSearchTerm(String searchTerm) { - this.searchTerm = searchTerm; - } - - public SearchType getSearchType() { - return searchType; - } - - public void setSearchType(SearchType searchType) { - this.searchType = searchType; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchType.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchType.java deleted file mode 100644 index 678f46d..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/dto/SearchType.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -/** - * Describes the search type of the search. Legal values are: - *
    - *
  • METHOD_NAME which means that the query is obtained from the method name of the query method.
  • - *
  • NAMED_QUERY which means that a named query is used.
  • - *
  • QUERY_ANNOTATION which means that the query method annotated with @Query annotation is used.
  • - *
- * @author Petri Kainulainen - */ -public enum SearchType { - METHOD_NAME, - NAMED_QUERY, - QUERY_ANNOTATION; -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java deleted file mode 100644 index d59fcc3..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import javax.persistence.*; -import java.util.Date; - -/** - * An entity class which contains the information of a single person. - * @author Petri Kainulainen - */ -@Entity -@NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(?1)") -@Table(name = "persons") -public class Person { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "creation_time", nullable = false) - private Date creationTime; - - @Column(name = "first_name", nullable = false) - private String firstName; - - @Column(name = "last_name", nullable = false) - private String lastName; - - @Column(name = "modification_time", nullable = false) - private Date modificationTime; - - @Version - private long version = 0; - - public Long getId() { - return id; - } - - /** - * Gets a builder which is used to create Person objects. - * @param firstName The first name of the created user. - * @param lastName The last name of the created user. - * @return A new Builder instance. - */ - public static Builder getBuilder(String firstName, String lastName) { - return new Builder(firstName, lastName); - } - - public Date getCreationTime() { - return creationTime; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - /** - * Gets the full name of the person. - * @return The full name of the person. - */ - @Transient - public String getName() { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - public Date getModificationTime() { - return modificationTime; - } - - public long getVersion() { - return version; - } - - public void update(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - @PreUpdate - public void preUpdate() { - modificationTime = new Date(); - } - - @PrePersist - public void prePersist() { - Date now = new Date(); - creationTime = now; - modificationTime = now; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * A Builder class used to create new Person objects. - */ - public static class Builder { - Person built; - - /** - * Creates a new Builder instance. - * @param firstName The first name of the created Person object. - * @param lastName The last name of the created Person object. - */ - Builder(String firstName, String lastName) { - built = new Person(); - built.firstName = firstName; - built.lastName = lastName; - } - - /** - * Builds the new Person object. - * @return The created Person object. - */ - public Person build() { - return built; - } - } - - /** - * This setter method should only be used by unit tests. - * @param id - */ - protected void setId(Long id) { - this.id = id; - } -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java deleted file mode 100644 index cd5f12b..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -/** - * Specifies methods used to obtain and modify person related information - * which is stored in the database. - * @author Petri Kainulainen - */ -public interface PersonRepository extends JpaRepository { - - /** - * Finds a person by using the last name as a search criteria. - * @param lastName - * @return A list of persons whose last name is an exact match with the given last name. - * If no persons is found, this method returns an empty list. - */ - @Query("SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(:lastName)") - public List find(@Param("lastName") String lastName); - - /** - * Finds person by using the last name as a search criteria. - * @param lastName - * @return A list of persons whose last name is an exact match with the given last name. - * If no persons is found, this method returns null. - */ - public List findByName(String lastName); - - /** - * Finds persons by using the last name as a search criteria. - * @param lastName - * @return A list of persons which last name is an exact match with the given last name. - * If no persons is found, this method returns an empty list. - */ - public List findByLastName(String lastName); -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java deleted file mode 100644 index 35cbd2e..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -/** - * This exception is thrown if the wanted person is not found. - * @author Petri Kainulainen - */ -public class PersonNotFoundException extends Exception { -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java deleted file mode 100644 index d615941..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.model.Person; - -import java.util.List; - -/** - * Declares methods used to obtain and modify person information. - * @author Petri Kainulainen - */ -public interface PersonService { - - /** - * Creates a new person. - * @param created The information of the created person. - * @return The created person. - */ - public Person create(PersonDTO created); - - /** - * Deletes a person. - * @param personId The id of the deleted person. - * @return The deleted person. - * @throws PersonNotFoundException if no person is found with the given id. - */ - public Person delete(Long personId) throws PersonNotFoundException; - - /** - * Finds all persons. - * @return A list of persons. - */ - public List findAll(); - - /** - * Finds person by id. - * @param id The id of the wanted person. - * @return The found person. If no person is found, this method returns null. - */ - public Person findById(Long id); - - /** - * Searches persons by using the search criteria given as a parameter. - * @param searchCriteria - * @return A list of persons matching with the search criteria. If no persons is found, this method - * returns an empty list. - * @throws IllegalArgumentException if search type is not given. - */ - public List search(SearchDTO searchCriteria); - - /** - * Updates the information of a person. - * @param updated The information of the updated person. - * @return The updated person. - * @throws PersonNotFoundException if no person is found with given id. - */ - public Person update(PersonDTO updated) throws PersonNotFoundException; -} diff --git a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java b/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java deleted file mode 100644 index d24764f..0000000 --- a/tutorial-part-three/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java +++ /dev/null @@ -1,127 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchType; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.List; - -/** - * This implementation of the PersonService interface communicates with - * the database by using a Spring Data JPA repository. - * @author Petri Kainulainen - */ -@Service -public class RepositoryPersonService implements PersonService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class); - - @Resource - private PersonRepository personRepository; - - @Transactional - @Override - public Person create(PersonDTO created) { - LOGGER.debug("Creating a new person with information: " + created); - - Person person = Person.getBuilder(created.getFirstName(), created.getLastName()).build(); - - return personRepository.save(person); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person delete(Long personId) throws PersonNotFoundException { - LOGGER.debug("Deleting person with id: " + personId); - - Person deleted = personRepository.findOne(personId); - - if (deleted == null) { - LOGGER.debug("No person found with id: " + personId); - throw new PersonNotFoundException(); - } - - personRepository.delete(deleted); - return deleted; - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - LOGGER.debug("Finding all persons"); - return personRepository.findAll(); - } - - @Transactional(readOnly = true) - @Override - public Person findById(Long id) { - LOGGER.debug("Finding person by id: " + id); - return personRepository.findOne(id); - } - - @Transactional(readOnly = true) - @Override - public List search(SearchDTO searchCriteria) { - LOGGER.debug("Searching persons with search criteria: " + searchCriteria); - - String searchTerm = searchCriteria.getSearchTerm(); - SearchType searchType = searchCriteria.getSearchType(); - - if (searchType == null) { - throw new IllegalArgumentException(); - } - - return findPersonsBySearchType(searchTerm, searchType); - } - - private List findPersonsBySearchType(String searchTerm, SearchType searchType) { - List persons; - - if (searchType == SearchType.METHOD_NAME) { - LOGGER.debug("Searching persons by using method name query creation."); - persons = personRepository.findByLastName(searchTerm); - } - else if (searchType == SearchType.NAMED_QUERY) { - LOGGER.debug("Searching persons by using named query"); - persons = personRepository.findByName(searchTerm); - } - else { - LOGGER.debug("Searching persons by using query annotation"); - persons = personRepository.find(searchTerm); - } - - return persons; - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person update(PersonDTO updated) throws PersonNotFoundException { - LOGGER.debug("Updating person with information: " + updated); - - Person person = personRepository.findOne(updated.getId()); - - if (person == null) { - LOGGER.debug("No person found with id: " + updated.getId()); - throw new PersonNotFoundException(); - } - - person.update(updated.getFirstName(), updated.getLastName()); - - return person; - } - - /** - * This setter method should be used only by unit tests. - * @param personRepository - */ - protected void setPersonRepository(PersonRepository personRepository) { - this.personRepository = personRepository; - } -} diff --git a/tutorial-part-three/src/main/resources/application.properties b/tutorial-part-three/src/main/resources/application.properties deleted file mode 100644 index 426c303..0000000 --- a/tutorial-part-three/src/main/resources/application.properties +++ /dev/null @@ -1,29 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.hbm2ddl.auto=create-drop -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-three/src/main/resources/applicationContext.xml b/tutorial-part-three/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-three/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-three/src/main/resources/i18n/messages.properties b/tutorial-part-three/src/main/resources/i18n/messages.properties deleted file mode 100644 index 0a82db4..0000000 --- a/tutorial-part-three/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,45 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part Two - - -person.list.link.label=View persons - -#Person list page -person.list.page.title=Persons -person.list.page.label.no.persons.found=No persons was found. -person.create.link.label=Create person -person.edit.link.label=Edit person -person.delete.link.label=Delete person -person.search.form.title=Search -person.search.form.submit.label=Search -person.search.searchterm.label=Search Term -person.search.searchtype.label=Search Type -person.search.result.page.title=Search Results for Search Term - -SearchType.METHOD_NAME=Method Name -SearchType.NAMED_QUERY=Named Query -SearchType.QUERY_ANNOTATION=Query Annotation - -#Create person page -person.create.page.title=Create Person -person.create.page.submit.label=Create - -#Edit person page -person.edit.page.title=Edit Person -person.edit.page.submit.label=Edit - -#General person labels -person.label.firstName=First name -person.label.lastName=Last name - -#Error messages -error.message.deleted.not.found=Deleted person was not found. -error.message.edited.not.found=Edited person was not found. - -#Feedback messages -feedback.message.person.created=Person with name {0} was created. -feedback.message.person.deleted=Person with name {0} was deleted. -feedback.message.person.edited=Person with name {0} was edited. - -#Validation error messages -NotEmpty.person.firstName=Enter first name -NotEmpty.person.lastName=Enter last name \ No newline at end of file diff --git a/tutorial-part-three/src/main/resources/log4j.properties b/tutorial-part-three/src/main/resources/log4j.properties deleted file mode 100644 index 5ad34eb..0000000 --- a/tutorial-part-three/src/main/resources/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.appender.Stdout=org.apache.log4j.ConsoleAppender -log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n - -log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG diff --git a/tutorial-part-three/src/main/resources/webdefault.xml b/tutorial-part-three/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-three/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/create.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/create.jsp deleted file mode 100644 index 508cf83..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/create.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/edit.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/edit.jsp deleted file mode 100644 index c9d5b53..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/edit.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -

-
- - -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/list.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/list.jsp deleted file mode 100644 index df816d3..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/list.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - -
- -
-
- -
-
-
- - - - \ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/navigation.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/navigation.jsp deleted file mode 100644 index b2ea406..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/navigation.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -
- | - -
diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/personList.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/personList.jsp deleted file mode 100644 index d9d5096..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/personList.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - -

- - - - - - - - - - - - - - - - - - -
">">
-
- -

- -

-
\ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp deleted file mode 100644 index 20dde8b..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchForm.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> - -
- -
- - -
-
- - - - - - -
-
- "/> -
-
-
\ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp b/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp deleted file mode 100644 index b47b78d..0000000 --- a/tutorial-part-three/src/main/webapp/WEB-INF/jsp/person/searchResults.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - - - -

:

- - - \ No newline at end of file diff --git a/tutorial-part-three/src/main/webapp/static/css/styles.css b/tutorial-part-three/src/main/webapp/static/css/styles.css deleted file mode 100644 index 5ac2da3..0000000 --- a/tutorial-part-three/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,31 +0,0 @@ -body { - font-family: Verdana -} - -.error { - color: #ff0000; -} - -.messageblock { - color: #000; - background-color: #cbf7c8; - border: 3px solid #3bdb2a; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} - -.errorblock { - color: #000; - background-color: #ffEEEE; - border: 3px solid #ff0000; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} \ No newline at end of file diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java deleted file mode 100644 index cfce15c..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.petrikainulainen.spring.datajpa.context; - - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * A test context which is used for unit testing controllers. - * @author Petri Kainulainen - */ -@Configuration -public class TestContext { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} \ No newline at end of file diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java deleted file mode 100644 index 2ca7fd7..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.Locale; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class AbstractControllerTest { - - private static final String ERROR_MESSAGE = "errorMessage"; - private static final String ERROR_MESSAGE_CODE = "errorMessageCode"; - private static final String FEEDBACK_MESSAGE = "feedbackMessage"; - private static final String FEEDBACK_MESSAGE_CODE = "feedbackMessageCode"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String REDIRECT_PATH = "/foo"; - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private TestController controller; - - private MessageSource messageSourceMock; - - @Before - public void setUp() { - controller = new TestController(); - - messageSourceMock = mock(MessageSource.class); - controller.setMessageSource(messageSourceMock); - } - - @Test - public void addErrorMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(ERROR_MESSAGE); - - controller.addErrorMessage(model, ERROR_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String errorMessage = (String) model.getFlashAttributes().get(FLASH_ERROR_MESSAGE); - assertEquals(ERROR_MESSAGE, errorMessage); - } - - @Test - public void addFeedbackMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - - controller.addFeedbackMessage(model, FEEDBACK_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String feedbackMessage = (String) model.getFlashAttributes().get(FLASH_FEEDBACK_MESSAGE); - assertEquals(FEEDBACK_MESSAGE, feedbackMessage); - } - - @Test - public void createRedirectViewPath() { - String redirectView = controller.createRedirectViewPath(REDIRECT_PATH); - String expectedView = buildExpectedRedirectViewPath(REDIRECT_PATH); - - verifyZeroInteractions(messageSourceMock); - assertEquals(expectedView, redirectView); - } - - private String buildExpectedRedirectViewPath(String redirectPath) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(redirectPath); - return builder.toString(); - } - - - private class TestController extends AbstractController { - - } -} diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java deleted file mode 100644 index e83178c..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.context.TestContext; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.context.MessageSource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.when; - -/** - * An abstract base class for all controller unit tests. - * @author Petri Kainulainen - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {TestContext.class}) -public abstract class AbstractTestController { - - protected static final String ERROR_MESSAGE = "errorMessage"; - protected static final String FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private MessageSource messageSourceMock; - - @Resource - private Validator validator; - - @Before - public void setUp() { - messageSourceMock = mock(MessageSource.class); - setUpTest(); - } - - protected abstract void setUpTest(); - - /** - * Asserts that an error message is present. - * @param model The model which is used to store the error message. - * @param messageCode The message code of the expected error message. - */ - protected void assertErrorMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_ERROR_MESSAGE); - } - - /** - * Asserts that a feedback message is present. - * @param model The model which is used to store the feedback message. - * @param messageCode - */ - protected void assertFeedbackMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_FEEDBACK_MESSAGE); - } - - private void assertFlashMessages(RedirectAttributes model, String messageCode, String flashMessageParameterName) { - Map flashMessages = model.getFlashAttributes(); - Object message = flashMessages.get(flashMessageParameterName); - assertNotNull(message); - flashMessages.remove(message); - assertTrue(flashMessages.isEmpty()); - - verify(messageSourceMock, times(1)).getMessage(eq(messageCode), any(Object[].class), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - } - - /** - * Asserts that the binding result contains specified field errors. - * @param result The binding result - * @param fieldNames The names which should have validation errors. - */ - protected void assertFieldErrors(BindingResult result, String... fieldNames) { - assertEquals(fieldNames.length, result.getFieldErrorCount()); - for (String fieldName : fieldNames) { - assertNotNull(result.getFieldError(fieldName)); - } - } - - /** - * Binds and validates the given form object. - * @param request The http servlet request object. - * @param formObject A form object. - * @return A binding result containing the outcome of binding and validation. - */ - protected BindingResult bindAndValidate(HttpServletRequest request, Object formObject) { - WebDataBinder binder = new WebDataBinder(formObject); - binder.setValidator(validator); - binder.bind(new MutablePropertyValues(request.getParameterMap())); - binder.getValidator().validate(binder.getTarget(), binder.getBindingResult()); - return binder.getBindingResult(); - } - - /** - * Creates an expected redirect view path. - * @param path The path to the requested view. - * @return The expected redirect view path. - */ - protected String createExpectedRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * Initializes the message source mock to return an error message when - * the error message code given as a a parameter is used to get message - * from message source. - * @param errorMessageCode The wanted error message code. - */ - protected void initMessageSourceForErrorMessage(String errorMessageCode) { - when(messageSourceMock.getMessage(eq(errorMessageCode), any(Object[].class), any(Locale.class))).thenReturn(ERROR_MESSAGE); - } - - /** - * Initializes the message source mock to return a feedback message when - * the feedback message code given as a parameter is used to get message - * from message source. - * @param feedbackMessageCode The wanted feedback message code. - */ - protected void initMessageSourceForFeedbackMessage(String feedbackMessageCode) { - when(messageSourceMock.getMessage(eq(feedbackMessageCode), any(Object[].class), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - } - - /** - * Returns the message source mock. - * @return - */ - protected MessageSource getMessageSourceMock() { - return messageSourceMock; - } -} diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java deleted file mode 100644 index d7d0370..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java +++ /dev/null @@ -1,368 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchType; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.*; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonControllerTest extends AbstractTestController { - - private static final String FIELD_NAME_FIRST_NAME = "firstName"; - private static final String FIELD_NAME_LAST_NAME = "lastName"; - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private PersonController controller; - - private PersonService personServiceMock; - - @Override - public void setUpTest() { - controller = new PersonController(); - - controller.setMessageSource(getMessageSourceMock()); - - personServiceMock = mock(PersonService.class); - controller.setPersonService(personServiceMock); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.delete(PERSON_ID)).thenReturn(deleted); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personServiceMock.delete(PERSON_ID)).thenThrow(new PersonNotFoundException()); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void search() { - SearchDTO searchCriteria = createSearchCriteria(LAST_NAME, SearchType.METHOD_NAME); - List expected = new ArrayList(); - when(personServiceMock.search(searchCriteria)).thenReturn(expected); - - BindingAwareModelMap model = new BindingAwareModelMap(); - String view = controller.search(searchCriteria, model); - - verify(personServiceMock, times(1)).search(searchCriteria); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_SEARCH_RESULT_VIEW, view); - List actual = (List) model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS); - assertEquals(expected, actual); - } - - private SearchDTO createSearchCriteria(String searchTerm, SearchType searchType) { - SearchDTO searchCriteria = new SearchDTO(); - - searchCriteria.setSearchTerm(searchTerm); - searchCriteria.setSearchType(searchType); - - return searchCriteria; - } - - @Test - public void showCreatePersonForm() { - Model model = new BindingAwareModelMap(); - - String view = controller.showCreatePersonForm(model); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - - PersonDTO added = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - assertNotNull(added); - - assertNull(added.getId()); - assertNull(added.getFirstName()); - assertNull(added.getLastName()); - } - - @Test - public void submitCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME, LAST_NAME); - Person model = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.create(created)).thenReturn(model); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - - String expectedViewPath = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedViewPath, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - } - - @Test - public void submitEmptyCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = new PersonDTO(); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyFirstName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, null, LAST_NAME); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyLastName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, null); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_LAST_NAME); - } - - @Test - public void showEditPersonForm() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.findById(PERSON_ID)).thenReturn(person); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - - PersonDTO formObject = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - - assertNotNull(formObject); - assertEquals(person.getId(), formObject.getId()); - assertEquals(person.getFirstName(), formObject.getFirstName()); - assertEquals(person.getLastName(), formObject.getLastName()); - } - - @Test - public void showEditPersonFormWhenPersonIsNotFound() { - when(personServiceMock.findById(PERSON_ID)).thenReturn(null); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEditPersonForm() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenReturn(person); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - assertEquals(updated.getFirstName(), person.getFirstName()); - assertEquals(updated.getLastName(), person.getLastName()); - } - - @Test - public void submitEditPersonFormWhenPersonIsNotFound() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenThrow(new PersonNotFoundException()); - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEmptyEditPersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitEditPersonFormWhenFirstNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, LAST_NAME_UPDATED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitEditPersonFormWhenLastNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_LAST_NAME); - } - - @Test - public void showList() { - List persons = new ArrayList(); - when(personServiceMock.findAll()).thenReturn(persons); - - Model model = new BindingAwareModelMap(); - String view = controller.showList(model); - - verify(personServiceMock, times(1)).findAll(); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_LIST_VIEW, view); - assertEquals(persons, model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS)); - - SearchDTO searchCriteria = (SearchDTO) model.asMap().get(PersonController.MODEL_ATTRIBUTE_SEARCHCRITERIA); - assertNotNull(searchCriteria); - assertNull(searchCriteria.getSearchTerm()); - assertNull(searchCriteria.getSearchType()); - } -} diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java deleted file mode 100644 index de7a368..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.junit.Test; - -import java.util.Date; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Petri Kainulainen - */ -public class PersonTest { - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "Foo1"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "Bar1"; - - @Test - public void build() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - assertEquals(FIRST_NAME, built.getFirstName()); - assertEquals(LAST_NAME, built.getLastName()); - assertEquals(0, built.getVersion()); - - assertNull(built.getCreationTime()); - assertNull(built.getModificationTime()); - assertNull(built.getId()); - } - - @Test - public void getName() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - String expectedName = constructName(FIRST_NAME, LAST_NAME); - assertEquals(expectedName, built.getName()); - } - - private String constructName(String firstName, String lastName) { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - @Test - public void prePersist() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertEquals(creationTime, modificationTime); - } - - @Test - public void preUpdate() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //Back to work - } - - built.preUpdate(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertTrue(modificationTime.after(creationTime)); - } - - @Test - public void update() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.update(FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - assertEquals(FIRST_NAME_UPDATED, built.getFirstName()); - assertEquals(LAST_NAME_UPDATED, built.getLastName()); - } -} diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java deleted file mode 100644 index 6575587..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; - -/** - * An utility class which contains useful methods for unit testing person related - * functions. - * @author Petri Kainulainen - */ -public class PersonTestUtil { - - public static PersonDTO createDTO(Long id, String firstName, String lastName) { - PersonDTO dto = new PersonDTO(); - - dto.setId(id); - dto.setFirstName(firstName); - dto.setLastName(lastName); - - return dto; - } - - public static Person createModelObject(Long id, String firstName, String lastName) { - Person model = Person.getBuilder(firstName, lastName).build(); - - model.setId(id); - - return model; - } -} diff --git a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java b/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java deleted file mode 100644 index dc89541..0000000 --- a/tutorial-part-three/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchDTO; -import net.petrikainulainen.spring.datajpa.dto.SearchType; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class RepositoryPersonServiceTest { - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private RepositoryPersonService personService; - - private PersonRepository personRepositoryMock; - - @Before - public void setUp() { - personService = new RepositoryPersonService(); - - personRepositoryMock = mock(PersonRepository.class); - personService.setPersonRepository(personRepositoryMock); - } - - @Test - public void create() { - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, LAST_NAME); - Person persisted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.save(any(Person.class))).thenReturn(persisted); - - Person returned = personService.create(created); - - ArgumentCaptor personArgument = ArgumentCaptor.forClass(Person.class); - verify(personRepositoryMock, times(1)).save(personArgument.capture()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(created, personArgument.getValue()); - assertEquals(persisted, returned); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(deleted); - - Person returned = personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verify(personRepositoryMock, times(1)).delete(deleted); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(deleted, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(null); - - personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - } - - @Test - public void findAll() { - List persons = new ArrayList(); - when(personRepositoryMock.findAll()).thenReturn(persons); - - List returned = personService.findAll(); - - verify(personRepositoryMock, times(1)).findAll(); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(persons, returned); - } - - @Test - public void findById() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(person); - - Person returned = personService.findById(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(person, returned); - } - - @Test - public void searchWhenSearchTypeIsMethodName() { - SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.METHOD_NAME); - List expected = new ArrayList(); - when(personRepositoryMock.findByLastName(searchCriteria.getSearchTerm())).thenReturn(expected); - - List actual = personService.search(searchCriteria); - - verify(personRepositoryMock, times(1)).findByLastName(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(expected, actual); - } - - @Test - public void searchWhenSearchTypeIsNamedQuery() { - SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.NAMED_QUERY); - List expected = new ArrayList(); - when(personRepositoryMock.findByName(searchCriteria.getSearchTerm())).thenReturn(expected); - - List actual = personService.search(searchCriteria); - - verify(personRepositoryMock, times(1)).findByName(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(expected, actual); - } - - @Test - public void searchWhenSearchTypeIsQueryAnnotation() { - SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.QUERY_ANNOTATION); - List expected = new ArrayList(); - when(personRepositoryMock.find(searchCriteria.getSearchTerm())).thenReturn(expected); - - List actual = personService.search(searchCriteria); - - verify(personRepositoryMock, times(1)).find(searchCriteria.getSearchTerm()); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(expected, actual); - } - - @Test(expected = IllegalArgumentException.class) - public void searchWhenSearchTypeIsNull() { - SearchDTO searchCriteria = createSearchDTO(LAST_NAME, null); - - personService.search(searchCriteria); - - verifyZeroInteractions(personRepositoryMock); - } - - private SearchDTO createSearchDTO(String searchTerm, SearchType searchType) { - SearchDTO searchCriteria = new SearchDTO(); - searchCriteria.setSearchTerm(searchTerm); - searchCriteria.setSearchType(searchType); - return searchCriteria; - } - - @Test - public void update() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(person); - - Person returned = personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(updated, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void updateWhenPersonIsNotFound() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(null); - - personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - } - - private void assertPerson(PersonDTO expected, Person actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getFirstName(), actual.getFirstName()); - assertEquals(expected.getLastName(), expected.getLastName()); - } - -} diff --git a/tutorial-part-two/LICENSE b/tutorial-part-two/LICENSE deleted file mode 100644 index b333aa5..0000000 --- a/tutorial-part-two/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2011 Petri Kainulainen - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/tutorial-part-two/README b/tutorial-part-two/README deleted file mode 100644 index 99c59d6..0000000 --- a/tutorial-part-two/README +++ /dev/null @@ -1,13 +0,0 @@ -This an example application of my blog entry: - -Spring Data JPA Tutorial Part Two: CRUD - -http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-two-crud/ - -RUNNING THE APPLICATION: - -- Download and install Maven 3 (http://maven.apache.org/download.html#Installation). If you - have already installed Maven 3, you can skip this step. -- Go the root directory of project (The one which contains the pom.xml file) -- Run command mvn clean jetty:run -- Start your browser and go to the location: http://localhost:8080 diff --git a/tutorial-part-two/pom.xml b/tutorial-part-two/pom.xml deleted file mode 100644 index a23ce33..0000000 --- a/tutorial-part-two/pom.xml +++ /dev/null @@ -1,239 +0,0 @@ - - 4.0.0 - net.petrikainulainen.spring - data-jpa-tutorial-part-two - war - 0.1 - Spring Data JPA Tutorial Part Two - Spring Data JPA Tutorial Part Two - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - http://www.petrikainulainen.net - - - repository.jboss.org-public - JBoss repository - https://repository.jboss.org/nexus/content/groups/public - - - - 4.0.1.Final - 5.1.18 - 1.6.1 - 3.1.0.RELEASE - UTF-8 - - - - - commons-lang - commons-lang - 2.6 - - - - org.springframework - spring-beans - ${spring.version} - - - org.springframework - spring-core - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - cglib - cglib - 2.2.2 - - - - org.springframework.data - spring-data-jpa - 1.0.2.RELEASE - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - - com.h2database - h2 - 1.3.160 - - - - - - - - - - com.jolbox - bonecp - 0.7.1.RELEASE - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - javax.servlet - jstl - 1.2 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - 1.2.16 - - - - junit - junit - 4.9 - test - - - org.mockito - mockito-core - 1.8.5 - test - - - org.springframework - spring-test - ${spring.version} - test - - - - data-jpa-tutorial-part-two - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - false - - - - org.mortbay.jetty - jetty-maven-plugin - 8.1.0.RC2 - - 0 - - src/main/resources/webdefault.xml - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0 - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.1 - - - - - - - diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java deleted file mode 100644 index 7eed429..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/ApplicationContext.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import com.jolbox.bonecp.BoneCPDataSource; -import org.hibernate.ejb.HibernatePersistence; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.*; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.servlet.view.JstlView; - -import javax.annotation.Resource; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * An application context Java configuration class. The usage of Java configuration - * requires Spring Framework 3.0 or higher with following exceptions: - *
    - *
  • @EnableWebMvc annotation requires Spring Framework 3.1
  • - *
- * - * @author Petri Kainulainen - */ -@Configuration -@ComponentScan(basePackages = {"net.petrikainulainen.spring.datajpa.controller", - "net.petrikainulainen.spring.datajpa.service"}) -@EnableTransactionManagement -@EnableWebMvc -@ImportResource("classpath:applicationContext.xml") -@PropertySource("classpath:application.properties") -public class ApplicationContext { - - private static final String VIEW_RESOLVER_PREFIX = "/WEB-INF/jsp/"; - private static final String VIEW_RESOLVER_SUFFIX = ".jsp"; - - private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; - private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; - private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; - private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; - - private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; - private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql"; - private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto"; - private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy"; - private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; - private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; - - private static final String PROPERTY_NAME_MESSAGESOURCE_BASENAME = "message.source.basename"; - private static final String PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE = "message.source.use.code.as.default.message"; - - @Resource - private Environment environment; - - @Bean - public DataSource dataSource() { - BoneCPDataSource dataSource = new BoneCPDataSource(); - - dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); - dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); - dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); - dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); - - return dataSource; - } - - @Bean - public JpaTransactionManager transactionManager() throws ClassNotFoundException { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - - transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject()); - - return transactionManager; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws ClassNotFoundException { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - - entityManagerFactoryBean.setDataSource(dataSource()); - entityManagerFactoryBean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); - entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); - - Properties jpaProterties = new Properties(); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY)); - jpaProterties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); - - entityManagerFactoryBean.setJpaProperties(jpaProterties); - - return entityManagerFactoryBean; - } - - @Bean - public MessageSource messageSource() { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - - messageSource.setBasename(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_BASENAME)); - messageSource.setUseCodeAsDefaultMessage(Boolean.parseBoolean(environment.getRequiredProperty(PROPERTY_NAME_MESSAGESOURCE_USE_CODE_AS_DEFAULT_MESSAGE))); - - return messageSource; - } - - @Bean - public ViewResolver viewResolver() { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - - viewResolver.setViewClass(JstlView.class); - viewResolver.setPrefix(VIEW_RESOLVER_PREFIX); - viewResolver.setSuffix(VIEW_RESOLVER_SUFFIX); - - return viewResolver; - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java deleted file mode 100644 index e01aa56..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/config/DataJPAExampleInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.petrikainulainen.spring.datajpa.config; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.*; - -/** - * Web application Java configuration class. The usage of web application - * initializer requires Spring Framework 3.1 and Servlet 3.0. - * @author Petri Kainulainen - */ -public class DataJPAExampleInitializer implements WebApplicationInitializer { - - private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; - private static final String DISPATCHER_SERVLET_MAPPING = "/"; - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationContext.class); - - ServletRegistration.Dynamic dispatcher = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(rootContext)); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping(DISPATCHER_SERVLET_MAPPING); - - servletContext.addListener(new ContextLoaderListener(rootContext)); - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java deleted file mode 100644 index 83ed0b6..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/AbstractController.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import java.util.Locale; - -/** - * An abstract controller class which provides utility methods useful - * to actual controller classes. - * @author Petri Kainulainen - */ -public abstract class AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractController.class); - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - @Resource - private MessageSource messageSource; - - /** - * Adds a new error message - * @param model A model which stores the the error message. - * @param code A message code which is used to fetch the correct message from the message source. - * @param params The parameters attached to the actual error message. - */ - protected void addErrorMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("adding error message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedErrorMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedErrorMessage); - model.addFlashAttribute(FLASH_ERROR_MESSAGE, localizedErrorMessage); - } - - /** - * Adds a new feedback message. - * @param model A model which stores the feedback message. - * @param code A message code which is used to fetch the actual message from the message source. - * @param params The parameters which are attached to the actual feedback message. - */ - protected void addFeedbackMessage(RedirectAttributes model, String code, Object... params) { - LOGGER.debug("Adding feedback message with code: " + code + " and params: " + params); - Locale current = LocaleContextHolder.getLocale(); - LOGGER.debug("Current locale is " + current); - String localizedFeedbackMessage = messageSource.getMessage(code, params, current); - LOGGER.debug("Localized message is: " + localizedFeedbackMessage); - model.addFlashAttribute(FLASH_FEEDBACK_MESSAGE, localizedFeedbackMessage); - } - - /** - * Creates a redirect view path for a specific controller action - * @param path The path processed by the controller method. - * @return A redirect view path to the given controller method. - */ - protected String createRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * This method should only be used by unit tests. - * @param messageSource - */ - protected void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java deleted file mode 100644 index ea0d73c..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/controller/PersonController.java +++ /dev/null @@ -1,186 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.util.List; - -/** - * @author Petri Kainulainen - */ -@Controller -@SessionAttributes("person") -public class PersonController extends AbstractController { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); - - protected static final String ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND = "error.message.deleted.not.found"; - protected static final String ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND = "error.message.edited.not.found"; - - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_CREATED = "feedback.message.person.created"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_DELETED = "feedback.message.person.deleted"; - protected static final String FEEDBACK_MESSAGE_KEY_PERSON_EDITED = "feedback.message.person.edited"; - - protected static final String MODEL_ATTIRUTE_PERSON = "person"; - protected static final String MODEL_ATTRIBUTE_PERSONS = "persons"; - - protected static final String PERSON_ADD_FORM_VIEW = "person/create"; - protected static final String PERSON_EDIT_FORM_VIEW = "person/edit"; - protected static final String PERSON_LIST_VIEW = "person/list"; - - protected static final String REQUEST_MAPPING_LIST = "/"; - - @Resource - private PersonService personService; - - /** - * Processes delete person requests. - * @param id The id of the deleted person. - * @param attributes - * @return - */ - @RequestMapping(value = "/person/delete/{id}", method = RequestMethod.GET) - public String delete(@PathVariable("id") Long id, RedirectAttributes attributes) { - LOGGER.debug("Deleting person with id: " + id); - - try { - Person deleted = personService.delete(id); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_DELETED, deleted.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes create person requests. - * @param model - * @return The name of the create person form view. - */ - @RequestMapping(value = "/person/create", method = RequestMethod.GET) - public String showCreatePersonForm(Model model) { - LOGGER.debug("Rendering create person form"); - - model.addAttribute(MODEL_ATTIRUTE_PERSON, new PersonDTO()); - - return PERSON_ADD_FORM_VIEW; - } - - /** - * Processes the submissions of create person form. - * @param created The information of the created persons. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/create", method = RequestMethod.POST) - public String submitCreatePersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO created, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Create person form was submitted with information: " + created); - - if (bindingResult.hasErrors()) { - return PERSON_ADD_FORM_VIEW; - } - - Person person = personService.create(created); - - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_CREATED, person.getName()); - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - /** - * Processes edit person requests. - * @param id The id of the edited person. - * @param model - * @param attributes - * @return The name of the edit person form view. - */ - @RequestMapping(value = "/person/edit/{id}", method = RequestMethod.GET) - public String showEditPersonForm(@PathVariable("id") Long id, Model model, RedirectAttributes attributes) { - LOGGER.debug("Rendering edit person form for person with id: " + id); - - Person person = personService.findById(id); - if (person == null) { - LOGGER.debug("No person found with id: " + id); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - model.addAttribute(MODEL_ATTIRUTE_PERSON, constructFormObject(person)); - - return PERSON_EDIT_FORM_VIEW; - } - - /** - * Processes the submissions of edit person form. - * @param updated The information of the edited person. - * @param bindingResult - * @param attributes - * @return - */ - @RequestMapping(value = "/person/edit", method = RequestMethod.POST) - public String submitEditPersonForm(@Valid @ModelAttribute(MODEL_ATTIRUTE_PERSON) PersonDTO updated, BindingResult bindingResult, RedirectAttributes attributes) { - LOGGER.debug("Edit person form was submitted with information: " + updated); - - if (bindingResult.hasErrors()) { - LOGGER.debug("Edit person form contains validation errors. Rendering form view."); - return PERSON_EDIT_FORM_VIEW; - } - - try { - Person person = personService.update(updated); - addFeedbackMessage(attributes, FEEDBACK_MESSAGE_KEY_PERSON_EDITED, person.getName()); - } catch (PersonNotFoundException e) { - LOGGER.debug("No person was found with id: " + updated.getId()); - addErrorMessage(attributes, ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - return createRedirectViewPath(REQUEST_MAPPING_LIST); - } - - private PersonDTO constructFormObject(Person person) { - PersonDTO formObject = new PersonDTO(); - - formObject.setId(person.getId()); - formObject.setFirstName(person.getFirstName()); - formObject.setLastName(person.getLastName()); - - return formObject; - } - - /** - * Processes requests to home page which lists all available persons. - * @param model - * @return The name of the person list view. - */ - @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) - public String showList(Model model) { - LOGGER.debug("Rendering person list page"); - - List persons = personService.findAll(); - model.addAttribute(MODEL_ATTRIBUTE_PERSONS, persons); - - return PERSON_LIST_VIEW; - } - - /** - * This setter method should only be used by unit tests - * @param personService - */ - protected void setPersonService(PersonService personService) { - this.personService = personService; - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java deleted file mode 100644 index 881ddb6..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/dto/PersonDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.petrikainulainen.spring.datajpa.dto; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.hibernate.validator.constraints.NotEmpty; - - -/** - * A DTO object which is used as a form object - * in create person and edit person forms. - * @author Petri Kainulainen - */ -public class PersonDTO { - - private Long id; - - @NotEmpty - private String firstName; - - @NotEmpty - private String lastName; - - public PersonDTO() { - - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - 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; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java deleted file mode 100644 index ed236d6..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/model/Person.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import javax.persistence.*; -import java.util.Date; - -/** - * An entity class which contains the information of a single person. - * @author Petri Kainulainen - */ -@Entity -@Table(name = "persons") -public class Person { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "creation_time", nullable = false) - private Date creationTime; - - @Column(name = "first_name", nullable = false) - private String firstName; - - @Column(name = "last_name", nullable = false) - private String lastName; - - @Column(name = "modification_time", nullable = false) - private Date modificationTime; - - @Version - private long version = 0; - - public Long getId() { - return id; - } - - /** - * Gets a builder which is used to create Person objects. - * @param firstName The first name of the created user. - * @param lastName The last name of the created user. - * @return A new Builder instance. - */ - public static Builder getBuilder(String firstName, String lastName) { - return new Builder(firstName, lastName); - } - - public Date getCreationTime() { - return creationTime; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - /** - * Gets the full name of the person. - * @return The full name of the person. - */ - @Transient - public String getName() { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - public Date getModificationTime() { - return modificationTime; - } - - public long getVersion() { - return version; - } - - public void update(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - @PreUpdate - public void preUpdate() { - modificationTime = new Date(); - } - - @PrePersist - public void prePersist() { - Date now = new Date(); - creationTime = now; - modificationTime = now; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * A Builder class used to create new Person objects. - */ - public static class Builder { - Person built; - - /** - * Creates a new Builder instance. - * @param firstName The first name of the created Person object. - * @param lastName The last name of the created Person object. - */ - Builder(String firstName, String lastName) { - built = new Person(); - built.firstName = firstName; - built.lastName = lastName; - } - - /** - * Builds the new Person object. - * @return The created Person object. - */ - public Person build() { - return built; - } - } - - /** - * This setter method should only be used by unit tests. - * @param id - */ - protected void setId(Long id) { - this.id = id; - } -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java deleted file mode 100644 index 3101b41..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/repository/PersonRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.petrikainulainen.spring.datajpa.repository; - -import net.petrikainulainen.spring.datajpa.model.Person; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * Specifies methods used to obtain and modify person related information - * which is stored in the database. - * @author Petri Kainulainen - */ -public interface PersonRepository extends JpaRepository { -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java deleted file mode 100644 index 35cbd2e..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -/** - * This exception is thrown if the wanted person is not found. - * @author Petri Kainulainen - */ -public class PersonNotFoundException extends Exception { -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java deleted file mode 100644 index 2e15954..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/PersonService.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; - -import java.util.List; - -/** - * Declares methods used to obtain and modify person information. - * @author Petri Kainulainen - */ -public interface PersonService { - - /** - * Creates a new person. - * @param created The information of the created person. - * @return The created person. - */ - public Person create(PersonDTO created); - - /** - * Deletes a person. - * @param personId The id of the deleted person. - * @return The deleted person. - * @throws PersonNotFoundException if no person is found with the given id. - */ - public Person delete(Long personId) throws PersonNotFoundException; - - /** - * Finds all persons. - * @return A list of persons. - */ - public List findAll(); - - /** - * Finds person by id. - * @param id The id of the wanted person. - * @return The found person. If no person is found, this method returns null. - */ - public Person findById(Long id); - - /** - * Updates the information of a person. - * @param updated The information of the updated person. - * @return The updated person. - * @throws PersonNotFoundException if no person is found with given id. - */ - public Person update(PersonDTO updated) throws PersonNotFoundException; -} diff --git a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java b/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java deleted file mode 100644 index e241c7f..0000000 --- a/tutorial-part-two/src/main/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonService.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.List; - -/** - * This implementation of the PersonService interface communicates with - * the database by using a Spring Data JPA repository. - * @author Petri Kainulainen - */ -@Service -public class RepositoryPersonService implements PersonService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class); - - @Resource - private PersonRepository personRepository; - - @Transactional - @Override - public Person create(PersonDTO created) { - LOGGER.debug("Creating a new person with information: " + created); - - Person person = Person.getBuilder(created.getFirstName(), created.getLastName()).build(); - - return personRepository.save(person); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person delete(Long personId) throws PersonNotFoundException { - LOGGER.debug("Deleting person with id: " + personId); - - Person deleted = personRepository.findOne(personId); - - if (deleted == null) { - LOGGER.debug("No person found with id: " + personId); - throw new PersonNotFoundException(); - } - - personRepository.delete(deleted); - return deleted; - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - LOGGER.debug("Finding all persons"); - return personRepository.findAll(); - } - - @Transactional(readOnly = true) - @Override - public Person findById(Long id) { - LOGGER.debug("Finding person by id: " + id); - return personRepository.findOne(id); - } - - @Transactional(rollbackFor = PersonNotFoundException.class) - @Override - public Person update(PersonDTO updated) throws PersonNotFoundException { - LOGGER.debug("Updating person with information: " + updated); - - Person person = personRepository.findOne(updated.getId()); - - if (person == null) { - LOGGER.debug("No person found with id: " + updated.getId()); - throw new PersonNotFoundException(); - } - - person.update(updated.getFirstName(), updated.getLastName()); - - return person; - } - - /** - * This setter method should be used only by unit tests. - * @param personRepository - */ - protected void setPersonRepository(PersonRepository personRepository) { - this.personRepository = personRepository; - } -} diff --git a/tutorial-part-two/src/main/resources/application.properties b/tutorial-part-two/src/main/resources/application.properties deleted file mode 100644 index 426c303..0000000 --- a/tutorial-part-two/src/main/resources/application.properties +++ /dev/null @@ -1,29 +0,0 @@ -# The default database is H2 memory database but I have also -# added configuration needed to use either MySQL and PostgreSQL. - -#Database Configuration -db.driver=org.h2.Driver -#db.driver=com.mysql.jdbc.Driver -#db.driver=org.postgresql.Driver -db.url=jdbc:h2:mem:datajpa -#db.url=jdbc:mysql://localhost:3306/datajpa -#db.url=jdbc:postgresql://localhost/datajpa -db.username=sa -db.password= - -#Hibernate Configuration -hibernate.dialect=org.hibernate.dialect.H2Dialect -#hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect -#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -hibernate.format_sql=true -hibernate.hbm2ddl.auto=create-drop -hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy -hibernate.show_sql=true - -#MessageSource -message.source.basename=i18n/messages -message.source.use.code.as.default.message=true - -#EntityManager -#Declares the base package of the entity classes -entitymanager.packages.to.scan=net.petrikainulainen.spring.datajpa.model \ No newline at end of file diff --git a/tutorial-part-two/src/main/resources/applicationContext.xml b/tutorial-part-two/src/main/resources/applicationContext.xml deleted file mode 100644 index ad15504..0000000 --- a/tutorial-part-two/src/main/resources/applicationContext.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tutorial-part-two/src/main/resources/i18n/messages.properties b/tutorial-part-two/src/main/resources/i18n/messages.properties deleted file mode 100644 index 0d6ded7..0000000 --- a/tutorial-part-two/src/main/resources/i18n/messages.properties +++ /dev/null @@ -1,35 +0,0 @@ -spring.data.jpa.example.title=Spring Data JPA Tutorial Part Two - - -person.list.link.label=View persons - -#Person list page -person.list.page.title=Persons -person.create.link.label=Create person -person.edit.link.label=Edit person -person.delete.link.label=Delete person - -#Create person page -person.create.page.title=Create Person -person.create.page.submit.label=Create - -#Edit person page -person.edit.page.title=Edit Person -person.edit.page.submit.label=Edit - -#General person labels -person.label.firstName=First name -person.label.lastName=Last name - -#Error messages -error.message.deleted.not.found=Deleted person was not found. -error.message.edited.not.found=Edited person was not found. - -#Feedback messages -feedback.message.person.created=Person with name {0} was created. -feedback.message.person.deleted=Person with name {0} was deleted. -feedback.message.person.edited=Person with name {0} was edited. - -#Validation error messages -NotEmpty.person.firstName=Enter first name -NotEmpty.person.lastName=Enter last name \ No newline at end of file diff --git a/tutorial-part-two/src/main/resources/log4j.properties b/tutorial-part-two/src/main/resources/log4j.properties deleted file mode 100644 index 5ad34eb..0000000 --- a/tutorial-part-two/src/main/resources/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.appender.Stdout=org.apache.log4j.ConsoleAppender -log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n - -log4j.rootLogger=DEBUG,Stdout -log4j.logger.org.springframework=DEBUG diff --git a/tutorial-part-two/src/main/resources/webdefault.xml b/tutorial-part-two/src/main/resources/webdefault.xml deleted file mode 100644 index ffab3e6..0000000 --- a/tutorial-part-two/src/main/resources/webdefault.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - true - - - useFileMappedBuffer - true - - - resourceCache - resourceCache - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.apache.jasper.servlet.JspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - Disable TRACE - / - TRACE - - - - - \ No newline at end of file diff --git a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/create.jsp b/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/create.jsp deleted file mode 100644 index eeab669..0000000 --- a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/create.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - -

- -
- -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/edit.jsp b/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/edit.jsp deleted file mode 100644 index 29cce9b..0000000 --- a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/edit.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> - - - <spring:message code="spring.data.jpa.example.title"/> - - - -

- -
- - -
- : - - -
-
- : - - -
-
- "/> -
-
-
- - \ No newline at end of file diff --git a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/list.jsp b/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/list.jsp deleted file mode 100644 index 3859607..0000000 --- a/tutorial-part-two/src/main/webapp/WEB-INF/jsp/person/list.jsp +++ /dev/null @@ -1,39 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - <spring:message code="spring.data.jpa.example.title"/> - - - -
- -
-
- -
-
-
-

- - - - - - - - - - - - - - - - - - -
">">
- - \ No newline at end of file diff --git a/tutorial-part-two/src/main/webapp/static/css/styles.css b/tutorial-part-two/src/main/webapp/static/css/styles.css deleted file mode 100644 index 5ac2da3..0000000 --- a/tutorial-part-two/src/main/webapp/static/css/styles.css +++ /dev/null @@ -1,31 +0,0 @@ -body { - font-family: Verdana -} - -.error { - color: #ff0000; -} - -.messageblock { - color: #000; - background-color: #cbf7c8; - border: 3px solid #3bdb2a; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} - -.errorblock { - color: #000; - background-color: #ffEEEE; - border: 3px solid #ff0000; - border-radius: 5px; - border-style: solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - padding: 8px; - margin: 16px; -} \ No newline at end of file diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java deleted file mode 100644 index cfce15c..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/context/TestContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.petrikainulainen.spring.datajpa.context; - - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * A test context which is used for unit testing controllers. - * @author Petri Kainulainen - */ -@Configuration -public class TestContext { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} \ No newline at end of file diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java deleted file mode 100644 index 2ca7fd7..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.Locale; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class AbstractControllerTest { - - private static final String ERROR_MESSAGE = "errorMessage"; - private static final String ERROR_MESSAGE_CODE = "errorMessageCode"; - private static final String FEEDBACK_MESSAGE = "feedbackMessage"; - private static final String FEEDBACK_MESSAGE_CODE = "feedbackMessageCode"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String REDIRECT_PATH = "/foo"; - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private TestController controller; - - private MessageSource messageSourceMock; - - @Before - public void setUp() { - controller = new TestController(); - - messageSourceMock = mock(MessageSource.class); - controller.setMessageSource(messageSourceMock); - } - - @Test - public void addErrorMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(ERROR_MESSAGE); - - controller.addErrorMessage(model, ERROR_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(ERROR_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String errorMessage = (String) model.getFlashAttributes().get(FLASH_ERROR_MESSAGE); - assertEquals(ERROR_MESSAGE, errorMessage); - } - - @Test - public void addFeedbackMessage() { - RedirectAttributes model = new RedirectAttributesModelMap(); - Object[] params = new Object[0]; - when(messageSourceMock.getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - - controller.addFeedbackMessage(model, FEEDBACK_MESSAGE_CODE, params); - - verify(messageSourceMock, times(1)).getMessage(eq(FEEDBACK_MESSAGE_CODE), eq(params), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - - String feedbackMessage = (String) model.getFlashAttributes().get(FLASH_FEEDBACK_MESSAGE); - assertEquals(FEEDBACK_MESSAGE, feedbackMessage); - } - - @Test - public void createRedirectViewPath() { - String redirectView = controller.createRedirectViewPath(REDIRECT_PATH); - String expectedView = buildExpectedRedirectViewPath(REDIRECT_PATH); - - verifyZeroInteractions(messageSourceMock); - assertEquals(expectedView, redirectView); - } - - private String buildExpectedRedirectViewPath(String redirectPath) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(redirectPath); - return builder.toString(); - } - - - private class TestController extends AbstractController { - - } -} diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java deleted file mode 100644 index e83178c..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/AbstractTestController.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.context.TestContext; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.context.MessageSource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.when; - -/** - * An abstract base class for all controller unit tests. - * @author Petri Kainulainen - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {TestContext.class}) -public abstract class AbstractTestController { - - protected static final String ERROR_MESSAGE = "errorMessage"; - protected static final String FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String FLASH_ERROR_MESSAGE = "errorMessage"; - private static final String FLASH_FEEDBACK_MESSAGE = "feedbackMessage"; - - private static final String VIEW_REDIRECT_PREFIX = "redirect:"; - - private MessageSource messageSourceMock; - - @Resource - private Validator validator; - - @Before - public void setUp() { - messageSourceMock = mock(MessageSource.class); - setUpTest(); - } - - protected abstract void setUpTest(); - - /** - * Asserts that an error message is present. - * @param model The model which is used to store the error message. - * @param messageCode The message code of the expected error message. - */ - protected void assertErrorMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_ERROR_MESSAGE); - } - - /** - * Asserts that a feedback message is present. - * @param model The model which is used to store the feedback message. - * @param messageCode - */ - protected void assertFeedbackMessage(RedirectAttributes model, String messageCode) { - assertFlashMessages(model, messageCode, FLASH_FEEDBACK_MESSAGE); - } - - private void assertFlashMessages(RedirectAttributes model, String messageCode, String flashMessageParameterName) { - Map flashMessages = model.getFlashAttributes(); - Object message = flashMessages.get(flashMessageParameterName); - assertNotNull(message); - flashMessages.remove(message); - assertTrue(flashMessages.isEmpty()); - - verify(messageSourceMock, times(1)).getMessage(eq(messageCode), any(Object[].class), any(Locale.class)); - verifyNoMoreInteractions(messageSourceMock); - } - - /** - * Asserts that the binding result contains specified field errors. - * @param result The binding result - * @param fieldNames The names which should have validation errors. - */ - protected void assertFieldErrors(BindingResult result, String... fieldNames) { - assertEquals(fieldNames.length, result.getFieldErrorCount()); - for (String fieldName : fieldNames) { - assertNotNull(result.getFieldError(fieldName)); - } - } - - /** - * Binds and validates the given form object. - * @param request The http servlet request object. - * @param formObject A form object. - * @return A binding result containing the outcome of binding and validation. - */ - protected BindingResult bindAndValidate(HttpServletRequest request, Object formObject) { - WebDataBinder binder = new WebDataBinder(formObject); - binder.setValidator(validator); - binder.bind(new MutablePropertyValues(request.getParameterMap())); - binder.getValidator().validate(binder.getTarget(), binder.getBindingResult()); - return binder.getBindingResult(); - } - - /** - * Creates an expected redirect view path. - * @param path The path to the requested view. - * @return The expected redirect view path. - */ - protected String createExpectedRedirectViewPath(String path) { - StringBuilder builder = new StringBuilder(); - builder.append(VIEW_REDIRECT_PREFIX); - builder.append(path); - return builder.toString(); - } - - /** - * Initializes the message source mock to return an error message when - * the error message code given as a a parameter is used to get message - * from message source. - * @param errorMessageCode The wanted error message code. - */ - protected void initMessageSourceForErrorMessage(String errorMessageCode) { - when(messageSourceMock.getMessage(eq(errorMessageCode), any(Object[].class), any(Locale.class))).thenReturn(ERROR_MESSAGE); - } - - /** - * Initializes the message source mock to return a feedback message when - * the feedback message code given as a parameter is used to get message - * from message source. - * @param feedbackMessageCode The wanted feedback message code. - */ - protected void initMessageSourceForFeedbackMessage(String feedbackMessageCode) { - when(messageSourceMock.getMessage(eq(feedbackMessageCode), any(Object[].class), any(Locale.class))).thenReturn(FEEDBACK_MESSAGE); - } - - /** - * Returns the message source mock. - * @return - */ - protected MessageSource getMessageSourceMock() { - return messageSourceMock; - } -} diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java deleted file mode 100644 index 7a49e59..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/controller/PersonControllerTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package net.petrikainulainen.spring.datajpa.controller; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.service.PersonNotFoundException; -import net.petrikainulainen.spring.datajpa.service.PersonService; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.validation.support.BindingAwareModelMap; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; - -import java.util.*; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class PersonControllerTest extends AbstractTestController { - - private static final String FIELD_NAME_FIRST_NAME = "firstName"; - private static final String FIELD_NAME_LAST_NAME = "lastName"; - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private PersonController controller; - - private PersonService personServiceMock; - - @Override - public void setUpTest() { - controller = new PersonController(); - - controller.setMessageSource(getMessageSourceMock()); - - personServiceMock = mock(PersonService.class); - controller.setPersonService(personServiceMock); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.delete(PERSON_ID)).thenReturn(deleted); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_DELETED); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personServiceMock.delete(PERSON_ID)).thenThrow(new PersonNotFoundException()); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - String view = controller.delete(PERSON_ID, attributes); - - verify(personServiceMock, times(1)).delete(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_DELETED_PERSON_WAS_NOT_FOUND); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - } - - @Test - public void showCreatePersonForm() { - Model model = new BindingAwareModelMap(); - - String view = controller.showCreatePersonForm(model); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - - PersonDTO added = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - assertNotNull(added); - - assertNull(added.getId()); - assertNull(added.getFirstName()); - assertNull(added.getLastName()); - } - - @Test - public void submitCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME, LAST_NAME); - Person model = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.create(created)).thenReturn(model); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - - String expectedViewPath = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedViewPath, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_CREATED); - - verify(personServiceMock, times(1)).create(created); - verifyNoMoreInteractions(personServiceMock); - } - - @Test - public void submitEmptyCreatePersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = new PersonDTO(); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyFirstName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, null, LAST_NAME); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitCreatePersonFormWithEmptyLastName() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/create", "POST"); - - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, null); - - RedirectAttributes attributes = new RedirectAttributesModelMap(); - BindingResult result = bindAndValidate(mockRequest, created); - - String view = controller.submitCreatePersonForm(created, result, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_ADD_FORM_VIEW, view); - assertFieldErrors(result, FIELD_NAME_LAST_NAME); - } - - @Test - public void showEditPersonForm() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personServiceMock.findById(PERSON_ID)).thenReturn(person); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - - PersonDTO formObject = (PersonDTO) model.asMap().get(PersonController.MODEL_ATTIRUTE_PERSON); - - assertNotNull(formObject); - assertEquals(person.getId(), formObject.getId()); - assertEquals(person.getFirstName(), formObject.getFirstName()); - assertEquals(person.getLastName(), formObject.getLastName()); - } - - @Test - public void showEditPersonFormWhenPersonIsNotFound() { - when(personServiceMock.findById(PERSON_ID)).thenReturn(null); - - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - Model model = new BindingAwareModelMap(); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.showEditPersonForm(PERSON_ID, model, attributes); - - verify(personServiceMock, times(1)).findById(PERSON_ID); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEditPersonForm() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenReturn(person); - - initMessageSourceForFeedbackMessage(PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertFeedbackMessage(attributes, PersonController.FEEDBACK_MESSAGE_KEY_PERSON_EDITED); - - assertEquals(updated.getFirstName(), person.getFirstName()); - assertEquals(updated.getLastName(), person.getLastName()); - } - - @Test - public void submitEditPersonFormWhenPersonIsNotFound() throws PersonNotFoundException { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personServiceMock.update(updated)).thenThrow(new PersonNotFoundException()); - initMessageSourceForErrorMessage(PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verify(personServiceMock, times(1)).update(updated); - verifyNoMoreInteractions(personServiceMock); - - String expectedView = createExpectedRedirectViewPath(PersonController.REQUEST_MAPPING_LIST); - assertEquals(expectedView, view); - - assertErrorMessage(attributes, PersonController.ERROR_MESSAGE_KEY_EDITED_PERSON_WAS_NOT_FOUND); - } - - @Test - public void submitEmptyEditPersonForm() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME, FIELD_NAME_LAST_NAME); - } - - @Test - public void submitEditPersonFormWhenFirstNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, null, LAST_NAME_UPDATED); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_FIRST_NAME); - } - - @Test - public void submitEditPersonFormWhenLastNameIsEmpty() { - MockHttpServletRequest mockRequest = new MockHttpServletRequest("/person/edit", "POST"); - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, null); - - BindingResult bindingResult = bindAndValidate(mockRequest, updated); - RedirectAttributes attributes = new RedirectAttributesModelMap(); - - String view = controller.submitEditPersonForm(updated, bindingResult, attributes); - - verifyZeroInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_EDIT_FORM_VIEW, view); - assertFieldErrors(bindingResult, FIELD_NAME_LAST_NAME); - } - - @Test - public void showList() { - List persons = new ArrayList(); - when(personServiceMock.findAll()).thenReturn(persons); - - Model model = new BindingAwareModelMap(); - String view = controller.showList(model); - - verify(personServiceMock, times(1)).findAll(); - verifyNoMoreInteractions(personServiceMock); - - assertEquals(PersonController.PERSON_LIST_VIEW, view); - assertEquals(persons, model.asMap().get(PersonController.MODEL_ATTRIBUTE_PERSONS)); - } -} diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java deleted file mode 100644 index de7a368..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import org.junit.Test; - -import java.util.Date; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Petri Kainulainen - */ -public class PersonTest { - - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "Foo1"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "Bar1"; - - @Test - public void build() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - assertEquals(FIRST_NAME, built.getFirstName()); - assertEquals(LAST_NAME, built.getLastName()); - assertEquals(0, built.getVersion()); - - assertNull(built.getCreationTime()); - assertNull(built.getModificationTime()); - assertNull(built.getId()); - } - - @Test - public void getName() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - - String expectedName = constructName(FIRST_NAME, LAST_NAME); - assertEquals(expectedName, built.getName()); - } - - private String constructName(String firstName, String lastName) { - StringBuilder name = new StringBuilder(); - - name.append(firstName); - name.append(" "); - name.append(lastName); - - return name.toString(); - } - - @Test - public void prePersist() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertEquals(creationTime, modificationTime); - } - - @Test - public void preUpdate() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.prePersist(); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //Back to work - } - - built.preUpdate(); - - Date creationTime = built.getCreationTime(); - Date modificationTime = built.getModificationTime(); - - assertNotNull(creationTime); - assertNotNull(modificationTime); - assertTrue(modificationTime.after(creationTime)); - } - - @Test - public void update() { - Person built = Person.getBuilder(FIRST_NAME, LAST_NAME).build(); - built.update(FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - assertEquals(FIRST_NAME_UPDATED, built.getFirstName()); - assertEquals(LAST_NAME_UPDATED, built.getLastName()); - } -} diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java deleted file mode 100644 index 6575587..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/model/PersonTestUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.petrikainulainen.spring.datajpa.model; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; - -/** - * An utility class which contains useful methods for unit testing person related - * functions. - * @author Petri Kainulainen - */ -public class PersonTestUtil { - - public static PersonDTO createDTO(Long id, String firstName, String lastName) { - PersonDTO dto = new PersonDTO(); - - dto.setId(id); - dto.setFirstName(firstName); - dto.setLastName(lastName); - - return dto; - } - - public static Person createModelObject(Long id, String firstName, String lastName) { - Person model = Person.getBuilder(firstName, lastName).build(); - - model.setId(id); - - return model; - } -} diff --git a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java b/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java deleted file mode 100644 index bc40d6d..0000000 --- a/tutorial-part-two/src/test/java/net/petrikainulainen/spring/datajpa/service/RepositoryPersonServiceTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.petrikainulainen.spring.datajpa.service; - -import net.petrikainulainen.spring.datajpa.dto.PersonDTO; -import net.petrikainulainen.spring.datajpa.model.Person; -import net.petrikainulainen.spring.datajpa.model.PersonTestUtil; -import net.petrikainulainen.spring.datajpa.repository.PersonRepository; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author Petri Kainulainen - */ -public class RepositoryPersonServiceTest { - - private static final Long PERSON_ID = Long.valueOf(5); - private static final String FIRST_NAME = "Foo"; - private static final String FIRST_NAME_UPDATED = "FooUpdated"; - private static final String LAST_NAME = "Bar"; - private static final String LAST_NAME_UPDATED = "BarUpdated"; - - private RepositoryPersonService personService; - - private PersonRepository personRepositoryMock; - - @Before - public void setUp() { - personService = new RepositoryPersonService(); - - personRepositoryMock = mock(PersonRepository.class); - personService.setPersonRepository(personRepositoryMock); - } - - @Test - public void create() { - PersonDTO created = PersonTestUtil.createDTO(null, FIRST_NAME, LAST_NAME); - Person persisted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.save(any(Person.class))).thenReturn(persisted); - - Person returned = personService.create(created); - - ArgumentCaptor personArgument = ArgumentCaptor.forClass(Person.class); - verify(personRepositoryMock, times(1)).save(personArgument.capture()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(created, personArgument.getValue()); - assertEquals(persisted, returned); - } - - @Test - public void delete() throws PersonNotFoundException { - Person deleted = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(deleted); - - Person returned = personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verify(personRepositoryMock, times(1)).delete(deleted); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(deleted, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void deleteWhenPersonIsNotFound() throws PersonNotFoundException { - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(null); - - personService.delete(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - } - - @Test - public void findAll() { - List persons = new ArrayList(); - when(personRepositoryMock.findAll()).thenReturn(persons); - - List returned = personService.findAll(); - - verify(personRepositoryMock, times(1)).findAll(); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(persons, returned); - } - - @Test - public void findById() { - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - when(personRepositoryMock.findOne(PERSON_ID)).thenReturn(person); - - Person returned = personService.findById(PERSON_ID); - - verify(personRepositoryMock, times(1)).findOne(PERSON_ID); - verifyNoMoreInteractions(personRepositoryMock); - - assertEquals(person, returned); - } - - @Test - public void update() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - Person person = PersonTestUtil.createModelObject(PERSON_ID, FIRST_NAME, LAST_NAME); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(person); - - Person returned = personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - - assertPerson(updated, returned); - } - - @Test(expected = PersonNotFoundException.class) - public void updateWhenPersonIsNotFound() throws PersonNotFoundException { - PersonDTO updated = PersonTestUtil.createDTO(PERSON_ID, FIRST_NAME_UPDATED, LAST_NAME_UPDATED); - - when(personRepositoryMock.findOne(updated.getId())).thenReturn(null); - - personService.update(updated); - - verify(personRepositoryMock, times(1)).findOne(updated.getId()); - verifyNoMoreInteractions(personRepositoryMock); - } - - private void assertPerson(PersonDTO expected, Person actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getFirstName(), actual.getFirstName()); - assertEquals(expected.getLastName(), expected.getLastName()); - } - -}