├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── pom.xml └── src ├── main ├── frontend │ ├── .babelrc │ ├── .eslintrc.js │ ├── app │ │ └── index.js │ ├── package.json │ ├── style │ │ └── style.css │ └── webpack.config.js ├── java │ └── com │ │ └── dlizarra │ │ └── starter │ │ ├── AppConfig.java │ │ ├── DatabaseConfig.java │ │ ├── SecurityConfig.java │ │ ├── StarterApplication.java │ │ ├── StarterMain.java │ │ ├── StarterProfiles.java │ │ ├── role │ │ ├── Role.java │ │ └── RoleName.java │ │ ├── support │ │ ├── jpa │ │ │ ├── CustomCrudRepository.java │ │ │ ├── CustomJpaRepository.java │ │ │ ├── LocalDateTimeConverter.java │ │ │ └── ReadOnlyRepository.java │ │ ├── orika │ │ │ ├── LocalDateTimeConverter.java │ │ │ └── OrikaBeanMapper.java │ │ └── security │ │ │ ├── CurrentUser.java │ │ │ ├── CustomUserDetails.java │ │ │ └── CustomUserDetailsService.java │ │ └── user │ │ ├── User.java │ │ ├── UserController.java │ │ ├── UserDto.java │ │ ├── UserDtoMapper.java │ │ ├── UserNotFoundException.java │ │ ├── UserRepository.java │ │ ├── UserService.java │ │ └── UserServiceImpl.java └── resources │ ├── application-default.properties │ ├── application-production.properties │ ├── application-staging.properties │ ├── application.yml │ ├── data.sql │ └── static │ └── index.html └── test ├── java └── com │ └── dlizarra │ └── starter │ ├── support │ ├── AbstractIntegrationTest.java │ ├── AbstractUnitTest.java │ └── AbstractWebIntegrationTest.java │ └── user │ ├── UserRepositoryTest.java │ └── UserServiceTest.java └── resources └── sql ├── cleanup.sql └── user.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # Operating System Files 2 | *.DS_Store 3 | Thumbs.db 4 | 5 | # Build Files # 6 | bin 7 | target 8 | build/ 9 | .gradle 10 | 11 | # Eclipse Project Files # 12 | .classpath 13 | .project 14 | .settings 15 | 16 | # STS Project Files # 17 | .springBeans 18 | 19 | # IntelliJ IDEA Files # 20 | *.iml 21 | *.ipr 22 | *.iws 23 | *.idea 24 | 25 | # NetBeans Project Files # 26 | nbactions.xml 27 | nb-configuration.xml 28 | 29 | # Spring Roo 30 | log.roo 31 | 32 | # PMD 33 | .pmd 34 | 35 | # Node 36 | src/main/frontend/node_modules 37 | bundle.js 38 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: always 8 | after_success: 9 | - mvn clean cobertura:cobertura coveralls:report 10 | deploy: 11 | provider: heroku 12 | api_key: "heroku key" 13 | app: starter 14 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -Dspring.profiles.active=default -Dserver.port=$PORT -jar target/starter-0.0.1-SNAPSHOT.jar 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot and React starter app 2 | 3 | > Starter webapp using Spring Boot on the backend and React on the frontend, with 4 | Maven and Webpack as build tools, hot reloading on both sides and without xml configuration. 5 | 6 | ## Quickstart 7 | To run the app you just need to: 8 | 9 | git clone https://github.com/dlizarra/spring-boot-react-webpack-starter.git ./starter 10 | cd starter 11 | mvn spring-boot:run 12 | 13 | To check everything is running you can: 14 | 15 | # Visit the homepage 16 | http://localhost:8080 17 | 18 | # Go to the sample REST endpoint 19 | http://localhost:8080/api/users 20 | 21 | # Login to the H2 console (JDBC URL: 'jdbc:h2:mem:embedded', user = 'h2') 22 | http://localhost:8080/h2-console 23 | 24 | ## Start developing 25 | The Java code is available at `src/main/java` as usual, and the frontend files are in 26 | `src/main/frontend`. 27 | 28 | ### Running the backend 29 | Run `StarterMain` class from your IDE. 30 | 31 | ### Running the frontend 32 | Go to `src/main/frontend` and run `npm start`. (Run `npm install` before that if it's the first time) 33 | 34 | Now we should work with `localhost:9090` (this is where we'll see our live changes reflected) 35 | instead of `localhost:8080`. 36 | 37 | ### Hot reloading 38 | In the **backend** we make use of Spring DevTools to enable hot reloading, 39 | so every time we make a change in our files an application restart will 40 | be triggered automatically. 41 | 42 | Keep in mind that Spring DevTools automatic restart only works if we run the 43 | application by running the main method in our app, and not if for example we run 44 | the app with maven with `mvn spring-boot:run`. 45 | 46 | In the **frontend** we use Webpack Dev Server hot module replacement 47 | through the npm script `start`. Once the script is running the Dev Server will be 48 | watching for any changes on our frontend files. 49 | 50 | This way we can be really productive since we don't have to worry about recompiling and deploying 51 | our server or client side code every time we make changes. 52 | 53 | 54 | ### Profiles 55 | 56 | The project comes prepared for being used in three different environments plus 57 | another one for testing. We use Spring Profiles in combination with Boot feature for 58 | loading properties files by naming convention (application-*\*.properties). 59 | 60 | You can find the profile constants in 61 | [StarterProfiles](src/main/java/com/dlizarra/starter/StarterProfiles.java) 62 | and the properties files in `src/main/resources`. 63 | 64 | ### Database 65 | The database connections are configured in 66 | [DatabaseConfig](src/main/java/com/dlizarra/starter/DatabaseConfig.java) 67 | where we can find a working H2 embedded database connection for the default profile, and the staging and production configurations examples for working with an external database. 68 | 69 | ### Repository layer 70 | The project includes three base data repositories: 71 | 72 | - [ReadOnlyRepository](src/main/java/com/dlizarra/starter/support/jpa/ReadOnlyRepository.java): We can use this base repository when we want to make sure the application doesn't insert or update that type of entity, as it just exposes a set of methods to read entities. 73 | - [CustomCrudRepository](src/main/java/com/dlizarra/starter/support/jpa/CustomCrudRepository.java): It's the same as the `CrudRepository` that Spring Data provides, but the `findOne`method in the custom version returns a Java 8 `Optional` object instead of ``. It's just a small difference but it avoids having to override the `findOne` method in every repository to make it return an `Optional` object. This repository is intended to be used when we don't need paging or sorting capabilities for that entity. 74 | - [CustomJpaRepository](src/main/java/com/dlizarra/starter/support/jpa/CustomJpaRepository.java): Again, it's there to provide the same funcionality as the Spring `JpaRepository` but returning `Optional`. We can extend this base repository if we want CRUD operations plus paging and sorting capabilities. 75 | 76 | ### Security 77 | All the boilerplate for the initial Spring Security configuration is already created. These are they key classes: 78 | 79 | - [User](src/main/java/com/dlizarra/starter/user/User.java), [Role](src/main/java/com/dlizarra/starter/role/Role.java) and [RoleName](src/main/java/com/dlizarra/starter/role/RoleName.java) which are populated by [data.sql](src/main/resources/data.sql) file for the default profile only. 80 | - [CustomUserDetails](src/main/java/com/dlizarra/starter/support/security/CustomUserDetails.java) 81 | - [CustomUserDetailsService](src/main/java/com/dlizarra/starter/support/security/CustomUserDetailsService.java) 82 | - [SecurityConfig](src/main/java/com/dlizarra/starter/SecurityConfig.java) with just very basic security rules. 83 | 84 | ### DTO-Entity mapping 85 | The project includes Orika and it already has a class, [OrikaBeanMapper](src/main/java/com/dlizarra/starter/support/orika/OrikaBeanMapper.java), ready to be injected anywhere and be used to do any mapping. It will also scan the project on startup searching for custom mappers and components. 86 | 87 | You can see how to use it in [UserServiceImpl](src/main/java/com/dlizarra/starter/user/UserServiceImpl.java) or in this sample [project](https://github.com/dlizarra/orika-spring-integration). 88 | 89 | This, along with Lombok annotations for auto-generating getters, setters, toString methods and such, allows us to have much cleaner Entities and DTOs classes. 90 | 91 | ### Unit and integration testing 92 | For **unit testing** we included Spring Test, JUnit, Mockito and AssertJ as well as an [AbstractUnitTest](src/test/java/com/dlizarra/starter/support/AbstractUnitTest.java) class that we can extend to include the boilerplate annotations and configuration for every test. [UserServiceTest](src/test/java/com/dlizarra/starter/user/UserServiceTest.java) can serve as an example. 93 | 94 | To create integration tests we can extend [AbstractIntegrationTest](src/test/java/com/dlizarra/starter/support/AbstractIntegrationTest.java) and make use of Spring `@sql` annotation to run a databse script before every test, just like it's done in [UserRepositoryTest](src/test/java/com/dlizarra/starter/user/UserRepositoryTest.java). 95 | 96 | ### Code coverage 97 | The project is also ready to use Cobertura as a code coverage utility and Coveralls to show a nice graphical representation of the results, get a badge with the results, etc. 98 | 99 | The only thing you need to do is to create an account in [Coveralls.io](http://coveralls.io) and add your repo token key [here](pom.xml#L134) in the pom.xml. 100 | 101 | And if you want to use different tools you just need to remove the plugins from the pom. 102 | 103 | ### Linting 104 | We added ESLint preconfigured with Airbnb rules, which you can override and extend in [.eslintrc.js](src/main/frontend/.eslintrc.js) file. To lint the code you can run `npm run eslint` or configure your IDE/text editor to do so. 105 | 106 | ### Continuous integration and deployment 107 | A [travis.yml](.travis.yml) file is included with a minimal configuration just to use jdk 8, trigger the code analysis tool and deploy the app to Heroku using the `api_key` in the file. 108 | 109 | We also included a Heroku [Procfile](Procfile) which declares the `web` process type and the java command to run our app and specifies which Spring Profile we want to use. 110 | 111 | ### Other ways to run the app 112 | #### Run everything from Maven 113 | 114 | mvn generate-resources spring-boot:run 115 | 116 | The Maven goal `generate-resources` will execute the frontend-maven-plugin to install Node 117 | and Npm the first time, run npm install to download all the libraries that are not 118 | present already and tell webpack to generate our `bundle.js`. It's the equivalent of running `npm run build` or `npm start` on a terminal. 119 | 120 | #### Run Maven and Webpack separately (no hot-reloading) 121 | 122 | mvn spring-boot:run 123 | In a second terminal: 124 | 125 | cd src/main/frontend 126 | npm run build 127 | 128 | ## Tech stack and libraries 129 | ### Backend 130 | - [Spring Boot](http://projects.spring.io/spring-boot/) 131 | - [Spring MVC](http://docs.spring.io/autorepo/docs/spring/3.2.x/spring-framework-reference/html/mvc.html) 132 | - [Spring Data](http://projects.spring.io/spring-data/) 133 | - [Spring Security](http://projects.spring.io/spring-security/) 134 | - [Spring Test](http://docs.spring.io/autorepo/docs/spring-framework/3.2.x/spring-framework-reference/html/testing.html) 135 | - [JUnit](http://junit.org/) 136 | - [Mockito](http://mockito.org/) 137 | - [AssertJ](http://joel-costigliola.github.io/assertj/) 138 | - [Lombok](https://projectlombok.org/) 139 | - [Orika](http://orika-mapper.github.io/orika-docs/) 140 | - [Maven](https://maven.apache.org/) 141 | 142 | ### Frontend 143 | - [Node](https://nodejs.org/en/) 144 | - [React](https://facebook.github.io/react/) 145 | - [Redux](http://redux.js.org/) 146 | - [Webpack](https://webpack.github.io/) 147 | - [Axios](https://github.com/mzabriskie/axios) 148 | - [Babel](https://babeljs.io/) 149 | - [ES6](http://www.ecma-international.org/ecma-262/6.0/) 150 | - [ESLint](http://eslint.org/) 151 | 152 | --- 153 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.dlizarra 7 | starter 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | Spring Boot and React starter 12 | Spring Boot and React starter 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.0.M1 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 1.4.6 25 | 3.2.0 26 | 1.16.6 27 | 2.7 28 | 4.0.0 29 | 0.0.27 30 | v5.7.0 31 | 3.7.1 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | 42 | org.apache.tomcat 43 | tomcat-jdbc 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-web 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-security 63 | 64 | 65 | 66 | com.h2database 67 | h2 68 | 69 | 70 | org.postgresql 71 | postgresql 72 | runtime 73 | 74 | 75 | com.zaxxer 76 | HikariCP 77 | 78 | 79 | 80 | ma.glasnost.orika 81 | orika-core 82 | ${orika-core.version} 83 | 84 | 85 | org.projectlombok 86 | lombok 87 | ${lombok.version} 88 | provided 89 | 90 | 91 | org.assertj 92 | assertj-core 93 | ${assertj-core.version} 94 | 95 | 96 | org.hibernate 97 | hibernate-jpamodelgen 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-maven-plugin 106 | 107 | 108 | 109 | org.codehaus.mojo 110 | cobertura-maven-plugin 111 | ${cobertura-plugin.version} 112 | 113 | xml 114 | 256m 115 | true 116 | 117 | 118 | 119 | **/*_.java 120 | 121 | 122 | **/*_.class 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | org.eluder.coveralls 131 | coveralls-maven-plugin 132 | ${coveralls-plugin.version} 133 | 134 | your coveralls repository token goes here 135 | 136 | 137 | 138 | 139 | 140 | com.github.eirslett 141 | frontend-maven-plugin 142 | ${frontend-plugin.version} 143 | 144 | src/main/frontend 145 | https://nodejs.org/dist/ 146 | ${node.version} 147 | ${npm.version} 148 | target 149 | 150 | 151 | 152 | install node and npm 153 | 154 | install-node-and-npm 155 | 156 | generate-resources 157 | 158 | 159 | npm install 160 | 161 | npm 162 | 163 | 164 | install 165 | target 166 | 167 | 168 | 169 | webpack build 170 | 171 | webpack 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | spring-snapshots 182 | Spring Snapshots 183 | https://repo.spring.io/snapshot 184 | 185 | true 186 | 187 | 188 | 189 | spring-milestones 190 | Spring Milestones 191 | https://repo.spring.io/milestone 192 | 193 | false 194 | 195 | 196 | 197 | 198 | 199 | spring-snapshots 200 | Spring Snapshots 201 | https://repo.spring.io/snapshot 202 | 203 | true 204 | 205 | 206 | 207 | spring-milestones 208 | Spring Milestones 209 | https://repo.spring.io/milestone 210 | 211 | false 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/main/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"] 3 | } -------------------------------------------------------------------------------- /src/main/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/main/frontend/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '../style/style.css' 4 | 5 | ReactDOM.render( 6 |

App working

, 7 | document.querySelector('.container')); -------------------------------------------------------------------------------- /src/main/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "Starter app ", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack-dev-server --hot --inline ", 9 | "eslint": "eslint app" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dlizarra/spring-boot-react-webpack-starter.git" 14 | }, 15 | "license": "ISC", 16 | "devDependencies": { 17 | "babel-core": "^6.5.2", 18 | "babel-loader": "^6.2.4", 19 | "babel-preset-es2015": "^6.5.0", 20 | "babel-preset-react": "^6.5.0", 21 | "css-loader": "^0.23.1", 22 | "eslint": "^2.6.0", 23 | "eslint-config-airbnb": "^6.2.0", 24 | "eslint-plugin-react": "^4.2.3", 25 | "style-loader": "^0.13.0", 26 | "webpack": "^1.12.14", 27 | "webpack-dev-server": "^1.14.1", 28 | "webpack-hot-middleware": "^2.8.1", 29 | "webpack-merge": "^0.8.4" 30 | }, 31 | "dependencies": { 32 | "axios": "^0.9.1", 33 | "babel-preset-stage-1": "^6.5.0", 34 | "history": "^2.0.0", 35 | "lodash": "^4.5.1", 36 | "react": "^0.14.7", 37 | "react-dom": "^0.14.7", 38 | "react-redux": "^4.4.0", 39 | "react-router": "^2.0.0", 40 | "react-router-bootstrap": "^0.23.0", 41 | "react-router-redux": "^4.0.4", 42 | "redux": "^3.3.1", 43 | "redux-simple-router": "^2.0.4", 44 | "redux-thunk": "^2.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/frontend/style/style.css: -------------------------------------------------------------------------------- 1 | .testblue { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /src/main/frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | const TARGET = process.env.npm_lifecycle_event; 5 | const PATHS = { 6 | source: path.join(__dirname, 'app'), 7 | output: path.join(__dirname, '../../../target/classes/static') 8 | }; 9 | 10 | const common = { 11 | entry: [ 12 | PATHS.source 13 | ], 14 | output: { 15 | path: PATHS.output, 16 | publicPath: '', 17 | filename: 'bundle.js' 18 | }, 19 | module: { 20 | loaders: [{ 21 | exclude: /node_modules/, 22 | loader: 'babel' 23 | }, { 24 | test: /\.css$/, 25 | loader: 'style!css' 26 | }] 27 | }, 28 | resolve: { 29 | extensions: ['', '.js', '.jsx'] 30 | } 31 | }; 32 | 33 | if (TARGET === 'start' || !TARGET) { 34 | module.exports = merge(common, { 35 | devServer: { 36 | port: 9090, 37 | proxy: { 38 | '/': { 39 | target: 'http://localhost:8080', 40 | secure: false, 41 | prependPath: false 42 | } 43 | }, 44 | publicPath: 'http://localhost:9090/', 45 | historyApiFallback: true 46 | }, 47 | devtool: 'source-map' 48 | }); 49 | } 50 | 51 | if (TARGET === 'build') { 52 | module.exports = merge(common, {}); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | import org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; 7 | 8 | @SpringBootApplication 9 | @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }) 10 | public class AppConfig { 11 | // servlets, view resolvers... 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.context.annotation.PropertySource; 9 | import org.springframework.core.env.Environment; 10 | 11 | import com.zaxxer.hikari.HikariDataSource; 12 | 13 | @Configuration 14 | public class DatabaseConfig { 15 | 16 | @Profile({ StarterProfiles.STANDALONE, StarterProfiles.TEST }) 17 | @PropertySource("classpath:application-default.properties") // Not loaded by naming convention 18 | @Configuration 19 | static class StandaloneDatabaseConfig { 20 | @Bean 21 | public DataSource dataSource(final Environment env) { 22 | final HikariDataSource ds = new HikariDataSource(); 23 | ds.setJdbcUrl(env.getRequiredProperty("h2.jdbcurl")); 24 | ds.setUsername(env.getRequiredProperty("h2.username")); 25 | return ds; 26 | } 27 | } 28 | 29 | @Profile(StarterProfiles.STAGING) 30 | @Configuration 31 | static class StagingDatabaseConfig { 32 | @Bean 33 | public DataSource dataSource(final Environment env) { 34 | final HikariDataSource ds = new HikariDataSource(); 35 | ds.setJdbcUrl(env.getRequiredProperty("psql.jdbcurl")); 36 | ds.setUsername(env.getRequiredProperty("psql.username")); 37 | return ds; 38 | } 39 | } 40 | 41 | @Profile(StarterProfiles.PRODUCTION) 42 | @Configuration 43 | static class ProuctionDatabaseConfig { 44 | @Bean 45 | public DataSource dataSource(final Environment env) { 46 | final HikariDataSource ds = new HikariDataSource(); 47 | ds.setJdbcUrl(env.getRequiredProperty("psql.jdbcurl")); 48 | ds.setUsername(env.getRequiredProperty("psql.username")); 49 | return ds; 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 14 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 15 | import org.springframework.security.core.userdetails.UserDetailsService; 16 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | 19 | @EnableWebSecurity 20 | @Import({ SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }) 21 | @Profile("!test") 22 | @Configuration 23 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 24 | 25 | @Override 26 | protected void configure(final HttpSecurity http) throws Exception { 27 | http 28 | .authorizeRequests() 29 | .antMatchers("/resources/**", "/signup") 30 | .permitAll() 31 | .anyRequest() 32 | .authenticated() 33 | .and() 34 | .formLogin() 35 | .loginPage("/login") 36 | .permitAll() 37 | .and() 38 | .logout() 39 | .permitAll(); 40 | http 41 | .authorizeRequests() 42 | .anyRequest() 43 | .permitAll(); 44 | http.authorizeRequests().antMatchers("/").permitAll().and() 45 | .authorizeRequests().antMatchers("/console/**").permitAll(); 46 | 47 | http.csrf().disable(); 48 | http.headers().frameOptions().disable(); 49 | } 50 | 51 | @Bean 52 | public PasswordEncoder passwordEncoder() { 53 | return new BCryptPasswordEncoder(); 54 | } 55 | 56 | @Bean 57 | public DaoAuthenticationProvider daoAuthenticationProvider(final UserDetailsService userDetailsService) { 58 | final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 59 | authProvider.setUserDetailsService(userDetailsService); 60 | authProvider.setPasswordEncoder(passwordEncoder()); 61 | return authProvider; 62 | } 63 | 64 | @Autowired 65 | public void configureGlobal(final AuthenticationManagerBuilder auth, final UserDetailsService userDetailsService) 66 | throws Exception { 67 | auth 68 | .authenticationProvider(daoAuthenticationProvider(userDetailsService)) 69 | .userDetailsService(userDetailsService) 70 | .passwordEncoder(passwordEncoder()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/StarterApplication.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.core.env.ConfigurableEnvironment; 5 | 6 | public class StarterApplication extends SpringApplication { 7 | 8 | public StarterApplication(final Class clazz) { 9 | super(clazz); 10 | } 11 | 12 | @Override 13 | protected void configureProfiles(final ConfigurableEnvironment environment, final String[] args) { 14 | super.configureProfiles(environment, args); 15 | 16 | final boolean standaloneActive = environment.acceptsProfiles(StarterProfiles.STANDALONE); 17 | final boolean stagingActive = environment.acceptsProfiles(StarterProfiles.STAGING); 18 | final boolean productionActive = environment.acceptsProfiles(StarterProfiles.PRODUCTION); 19 | 20 | if (stagingActive && productionActive) { 21 | throw new IllegalStateException("Cannot activate staging and production profiles at the same time."); 22 | } else if (productionActive || stagingActive) { 23 | System.out.println("Activating " + 24 | (productionActive ? StarterProfiles.PRODUCTION : StarterProfiles.STAGING) + " profile."); 25 | } else if (standaloneActive) { 26 | System.out.println("Activating " + 27 | "the default standalone profile."); 28 | } else { 29 | throw new IllegalStateException( 30 | "Unknown profiles specified. Please specify one of default, staging or production."); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/StarterMain.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | public class StarterMain { 4 | 5 | public static void main(final String... args) { 6 | new StarterApplication(AppConfig.class).run(args); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/StarterProfiles.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter; 2 | 3 | public final class StarterProfiles { 4 | 5 | public static final String STANDALONE = "default"; 6 | public static final String TEST = "test"; 7 | public static final String STAGING = "staging"; 8 | public static final String PRODUCTION = "production"; 9 | 10 | private StarterProfiles() { 11 | // Non-instantiable class 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/role/Role.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.role; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.EnumType; 6 | import javax.persistence.Enumerated; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | 13 | import com.dlizarra.starter.user.User; 14 | 15 | import lombok.EqualsAndHashCode; 16 | import lombok.Getter; 17 | import lombok.Setter; 18 | import lombok.ToString; 19 | 20 | @EqualsAndHashCode(exclude = { "id", "user" }) 21 | @ToString 22 | @Getter 23 | @Setter 24 | @Entity 25 | public class Role { 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Integer id; 30 | 31 | @Enumerated(EnumType.STRING) 32 | @Column(nullable = false) 33 | private RoleName rolename; 34 | 35 | @JoinColumn(name = "user_id", nullable = false) 36 | @ManyToOne 37 | private User user; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/role/RoleName.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.role; 2 | 3 | public enum RoleName { 4 | ROLE_ADMIN, ROLE_USER 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/jpa/CustomCrudRepository.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.jpa; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import org.springframework.data.repository.NoRepositoryBean; 8 | import org.springframework.data.repository.Repository; 9 | 10 | @NoRepositoryBean 11 | public interface CustomCrudRepository extends Repository { 12 | /** 13 | * Saves a given entity. Use the returned instance for further operations as the save operation might have changed 14 | * the entity instance completely. 15 | * 16 | * @param entity 17 | * @return the saved entity 18 | */ 19 | S save(S entity); 20 | 21 | /** 22 | * Saves all given entities. 23 | * 24 | * @param entities 25 | * @return the saved entities 26 | * @throws IllegalArgumentException in case the given entity is {@literal null}. 27 | */ 28 | List save(Iterable entities); 29 | 30 | /** 31 | * Retrieves an entity by its id. 32 | * 33 | * @param id must not be {@literal null}. 34 | * @return the entity with the given id or {@literal null} if none found 35 | * @throws IllegalArgumentException if {@code id} is {@literal null} 36 | */ 37 | Optional findOne(ID id); 38 | 39 | /** 40 | * Returns whether an entity with the given id exists. 41 | * 42 | * @param id must not be {@literal null}. 43 | * @return true if an entity with the given id exists, {@literal false} otherwise 44 | * @throws IllegalArgumentException if {@code id} is {@literal null} 45 | */ 46 | boolean exists(ID id); 47 | 48 | /** 49 | * Returns all instances of the type. 50 | * 51 | * @return all entities 52 | */ 53 | List findAll(); 54 | 55 | /** 56 | * Returns all instances of the type with the given IDs. 57 | * 58 | * @param ids 59 | * @return 60 | */ 61 | List findAll(Iterable ids); 62 | 63 | /** 64 | * Returns the number of entities available. 65 | * 66 | * @return the number of entities 67 | */ 68 | long count(); 69 | 70 | /** 71 | * Deletes the entity with the given id. 72 | * 73 | * @param id must not be {@literal null}. 74 | * @throws IllegalArgumentException in case the given {@code id} is {@literal null} 75 | */ 76 | void delete(ID id); 77 | 78 | /** 79 | * Deletes a given entity. 80 | * 81 | * @param entity 82 | * @throws IllegalArgumentException in case the given entity is {@literal null}. 83 | */ 84 | void delete(T entity); 85 | 86 | /** 87 | * Deletes the given entities. 88 | * 89 | * @param entities 90 | * @throws IllegalArgumentException in case the given {@link Iterable} is {@literal null}. 91 | */ 92 | void delete(Iterable entities); 93 | 94 | /** 95 | * Deletes all entities managed by the repository. 96 | */ 97 | void deleteAll(); 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/jpa/CustomJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.jpa; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | import javax.persistence.EntityManager; 7 | 8 | import com.dlizarra.starter.support.jpa.CustomCrudRepository; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.data.jpa.repository.Query; 13 | import org.springframework.data.repository.NoRepositoryBean; 14 | 15 | @NoRepositoryBean 16 | public interface CustomJpaRepository extends CustomCrudRepository { 17 | 18 | /** 19 | * Returns all instances of the type sorted by the type. 20 | * 21 | * @param sort {@link Sort} object applied to the returned elements list. 22 | * @return ordered list 23 | */ 24 | List findAll(Sort sort); 25 | 26 | /** 27 | * Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object. 28 | * 29 | * @param pageable object for pagination information. 30 | * @return a page of entities 31 | */ 32 | Page findAll(Pageable pageable); 33 | 34 | /** 35 | * Flushes all pending changes to the database. 36 | */ 37 | void flush(); 38 | 39 | /** 40 | * Saves an entity and flushes changes instantly. 41 | * 42 | * @param entity 43 | * @return the saved entity 44 | */ 45 | S saveAndFlush(S entity); 46 | 47 | /** 48 | * Deletes the given entities in a batch which means it will create a single {@link Query}. Assume that we will 49 | * clear the {@link javax.persistence.EntityManager} after the call. 50 | * 51 | * @param entities 52 | */ 53 | void deleteInBatch(Iterable entities); 54 | 55 | /** 56 | * Deletes all entites in a batch call. 57 | */ 58 | void deleteAllInBatch(); 59 | 60 | /** 61 | * Returns a lazy-loaded proxy object of an entity with only its id populated. The rest of the data will be 62 | * populated on demand only when it's needed. Used for saving the cost of bringing a heavy object from database. The 63 | * implementation uses JPA's EntityManager.getReference method. 64 | * 65 | * @param id must not be {@literal null}. 66 | * @return a proxy object of the entity with the given identifier. 67 | * @see EntityManager#getReference(Class, Object) 68 | */ 69 | T getOne(ID id); 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/jpa/LocalDateTimeConverter.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.jpa; 2 | 3 | import java.sql.Timestamp; 4 | import java.time.LocalDateTime; 5 | 6 | import javax.persistence.AttributeConverter; 7 | import javax.persistence.Converter; 8 | 9 | @Converter(autoApply = true) 10 | public class LocalDateTimeConverter implements AttributeConverter { 11 | 12 | @Override 13 | public Timestamp convertToDatabaseColumn(final LocalDateTime locDateTime) { 14 | return (locDateTime == null ? null : Timestamp.valueOf(locDateTime)); 15 | } 16 | 17 | @Override 18 | public LocalDateTime convertToEntityAttribute(final Timestamp sqlTimestamp) { 19 | return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/jpa/ReadOnlyRepository.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.jpa; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.domain.Sort; 10 | import org.springframework.data.repository.NoRepositoryBean; 11 | import org.springframework.data.repository.Repository; 12 | 13 | @NoRepositoryBean 14 | public interface ReadOnlyRepository extends Repository { 15 | 16 | /** 17 | * Retrieves an entity by its id. 18 | * 19 | * @param id - must not be null. 20 | * @return the entity with the given id or null if none found. 21 | */ 22 | Optional findOne(ID id); 23 | 24 | /** 25 | * Returns all instances of the type. 26 | * 27 | * @return 28 | */ 29 | List findAll(); 30 | 31 | /** 32 | * Returns all instances of the type sorted by the type. 33 | * 34 | * @param sort {@link Sort} object applied to the returned elements list. 35 | * @return sorted list 36 | */ 37 | List findAll(Sort sort); 38 | 39 | /** 40 | * Returns a Page of entities meeting the paging restriction provided in the Pageable object. 41 | * 42 | * @param pageable object for pagination information. 43 | * @return A Page of entities. 44 | */ 45 | Page findAll(Pageable pageable); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/orika/LocalDateTimeConverter.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.orika; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | import ma.glasnost.orika.converter.BidirectionalConverter; 8 | import ma.glasnost.orika.metadata.Type; 9 | 10 | /** 11 | * Converter needed to avoid known issue when mapping LocalDateTime objects with Orika. 12 | * @see 13 | * http://stackoverflow.com/questions/30805753/how-to-map-java-time-localdate-field-with-orika 14 | */ 15 | @Component 16 | public class LocalDateTimeConverter extends BidirectionalConverter { 17 | 18 | @Override 19 | public LocalDateTime convertTo(final LocalDateTime source, final Type destinationType) { 20 | return source; 21 | } 22 | 23 | @Override 24 | public LocalDateTime convertFrom(final LocalDateTime source, final Type destinationType) { 25 | return source; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/orika/OrikaBeanMapper.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.orika; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.context.ApplicationContextAware; 8 | import org.springframework.stereotype.Component; 9 | 10 | import ma.glasnost.orika.Converter; 11 | import ma.glasnost.orika.Mapper; 12 | import ma.glasnost.orika.MapperFactory; 13 | import ma.glasnost.orika.converter.ConverterFactory; 14 | import ma.glasnost.orika.impl.ConfigurableMapper; 15 | import ma.glasnost.orika.impl.DefaultMapperFactory; 16 | import ma.glasnost.orika.metadata.ClassMapBuilder; 17 | 18 | /** 19 | * Orika mapper exposed as a Spring Bean. It contains the configuration for the mapper factory and factory builder. It 20 | * will scan the Spring application context searching for mappers and converters to register them into the factory. To 21 | * use it just autowire it into a class. 22 | * 23 | * @author dlizarra 24 | */ 25 | @Component 26 | public class OrikaBeanMapper extends ConfigurableMapper implements ApplicationContextAware { 27 | 28 | private MapperFactory factory; 29 | private ApplicationContext applicationContext; 30 | 31 | public OrikaBeanMapper() { 32 | super(false); 33 | } 34 | 35 | @Override 36 | protected void configure(final MapperFactory factory) { 37 | this.factory = factory; 38 | addAllSpringBeans(applicationContext); 39 | } 40 | 41 | @Override 42 | protected void configureFactoryBuilder(final DefaultMapperFactory.Builder factoryBuilder) { 43 | factoryBuilder.mapNulls(false); 44 | } 45 | 46 | /** 47 | * Constructs and registers a {@link ClassMapBuilder} into the {@link MapperFactory} using a {@link Mapper}. 48 | * 49 | * @param mapper 50 | */ 51 | @SuppressWarnings({ "rawtypes", "unchecked" }) 52 | public void addMapper(final Mapper mapper) { 53 | factory.classMap(mapper.getAType(), 54 | mapper.getBType()) 55 | .byDefault() 56 | .customize((Mapper) mapper) 57 | .mapNulls(false) 58 | .mapNullsInReverse(false) 59 | .register(); 60 | } 61 | 62 | /** 63 | * Registers a {@link Converter} into the {@link ConverterFactory}. 64 | * 65 | * @param converter 66 | */ 67 | public void addConverter(final Converter converter) { 68 | factory.getConverterFactory().registerConverter(converter); 69 | } 70 | 71 | /** 72 | * Scans the appliaction context and registers all Mappers and Converters found in it. 73 | * 74 | * @param applicationContext 75 | */ 76 | @SuppressWarnings("rawtypes") 77 | private void addAllSpringBeans(final ApplicationContext applicationContext) { 78 | final Map mappers = applicationContext.getBeansOfType(Mapper.class); 79 | for (final Mapper mapper : mappers.values()) { 80 | addMapper(mapper); 81 | } 82 | final Map converters = applicationContext.getBeansOfType(Converter.class); 83 | for (final Converter converter : converters.values()) { 84 | addConverter(converter); 85 | } 86 | } 87 | 88 | @Override 89 | public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { 90 | this.applicationContext = applicationContext; 91 | init(); 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/security/CurrentUser.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.security; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 10 | 11 | @Target(ElementType.PARAMETER) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Documented 14 | @AuthenticationPrincipal 15 | public @interface CurrentUser { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/security/CustomUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.security; 2 | 3 | import java.util.Collection; 4 | import java.util.stream.Collectors; 5 | 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | import com.dlizarra.starter.user.User; 11 | 12 | @SuppressWarnings("serial") 13 | public class CustomUserDetails extends User implements UserDetails { 14 | 15 | public CustomUserDetails(final User user) { 16 | super(user); 17 | } 18 | 19 | @Override 20 | public Collection getAuthorities() { 21 | return getRoles().stream() 22 | .map(role -> new SimpleGrantedAuthority(role.getRolename().name())) 23 | .collect(Collectors.toList()); 24 | 25 | } 26 | 27 | @Override 28 | public String getPassword() { 29 | return super.getPassword(); 30 | } 31 | 32 | @Override 33 | public boolean isAccountNonExpired() { 34 | return true; 35 | } 36 | 37 | @Override 38 | public boolean isAccountNonLocked() { 39 | return true; 40 | } 41 | 42 | @Override 43 | public boolean isCredentialsNonExpired() { 44 | return true; 45 | } 46 | 47 | @Override 48 | public boolean isEnabled() { 49 | return super.isEnabled(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/support/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support.security; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.dlizarra.starter.user.User; 10 | import com.dlizarra.starter.user.UserRepository; 11 | 12 | 13 | @Service 14 | public class CustomUserDetailsService implements UserDetailsService { 15 | 16 | @Autowired 17 | private UserRepository userRepository; 18 | 19 | @Override 20 | public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { 21 | final User user = userRepository.findByUsername(username) 22 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found.")); 23 | 24 | return new CustomUserDetails(user); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/User.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | // @formatter:off 4 | import java.time.LocalDateTime; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import javax.persistence.CascadeType; 9 | import javax.persistence.Column; 10 | import javax.persistence.Entity; 11 | import javax.persistence.FetchType; 12 | import javax.persistence.GeneratedValue; 13 | import javax.persistence.GenerationType; 14 | import javax.persistence.Id; 15 | import javax.persistence.OneToMany; 16 | import javax.persistence.PrePersist; 17 | import javax.persistence.PreUpdate; 18 | import javax.persistence.Table; 19 | 20 | import com.dlizarra.starter.support.security.CustomUserDetails; 21 | 22 | import com.dlizarra.starter.role.Role; 23 | 24 | import lombok.EqualsAndHashCode; 25 | import lombok.Getter; 26 | import lombok.Setter; 27 | import lombok.ToString; 28 | 29 | @EqualsAndHashCode(of = { "username", "roles", "enabled" }) 30 | @ToString(of = { "id", "username" }) 31 | @Setter 32 | @Getter 33 | @Entity 34 | @Table(name = "users") 35 | public class User { 36 | 37 | static final int MAX_LENGTH_USERNAME = 30; 38 | 39 | @Id 40 | @GeneratedValue(strategy = GenerationType.IDENTITY) 41 | private Integer id; 42 | 43 | @Column(nullable = false, unique = true, length = MAX_LENGTH_USERNAME) 44 | private String username; 45 | 46 | @Column(nullable = false) 47 | private String password; 48 | 49 | private boolean enabled; 50 | private LocalDateTime creationTime; 51 | private LocalDateTime modificationTime; 52 | 53 | @OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL) 54 | private Set roles = new HashSet(); 55 | 56 | public User() { 57 | } 58 | 59 | /** 60 | * Constructor used exclusively by {@link CustomUserDetails}} 61 | * 62 | * @param user 63 | */ 64 | public User(final User user) { 65 | this.id = user.id; 66 | this.username = user.username; 67 | this.password = user.password; 68 | this.enabled = user.enabled; 69 | } 70 | 71 | @PrePersist 72 | public void prePersist() { 73 | creationTime = LocalDateTime.now(); 74 | } 75 | 76 | @PreUpdate 77 | public void preUpdate() { 78 | modificationTime = LocalDateTime.now(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import java.util.List; 4 | 5 | import javax.validation.Valid; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestMethod; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import com.dlizarra.starter.role.RoleName; 15 | 16 | @RestController 17 | public class UserController { 18 | @Autowired 19 | private UserService userService; 20 | 21 | @RequestMapping(value = "/api/users", method = RequestMethod.GET) 22 | public List findAll() { 23 | return userService.getUsers(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserDto.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Set; 5 | 6 | import javax.validation.constraints.Size; 7 | 8 | import com.dlizarra.starter.role.Role; 9 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 10 | import com.fasterxml.jackson.annotation.JsonIgnore; 11 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 12 | 13 | import lombok.EqualsAndHashCode; 14 | import lombok.Getter; 15 | import lombok.Setter; 16 | import lombok.ToString; 17 | 18 | @EqualsAndHashCode(of = { "username", "roles", "enabled" }) 19 | @ToString(of = { "id", "username" }) 20 | @Setter 21 | @Getter 22 | @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 23 | public class UserDto { 24 | 25 | private Integer id; 26 | 27 | @Size(max = User.MAX_LENGTH_USERNAME) 28 | private String username; 29 | 30 | private String password; 31 | private boolean enabled; 32 | private LocalDateTime creationTime; 33 | private LocalDateTime modificationTime; 34 | @JsonIgnore 35 | private Set roles; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserDtoMapper.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import ma.glasnost.orika.CustomMapper; 6 | 7 | @Component 8 | public class UserDtoMapper extends CustomMapper { 9 | // @Override 10 | // public void mapAtoB(final User user, final UserDto userDto, final MappingContext context) { 11 | // userDto.getProjects().forEach(p -> { 12 | // if (p.getCreator() != null) { 13 | // p.getCreator().setProjects(new ArrayList()); 14 | // } 15 | // p.setMembers(new ArrayList()); 16 | // }); 17 | 18 | // userDto.getProjects().forEach(p -> { 19 | // if (p.getCreator() != null) { 20 | // p.getCreator().setProjects(new ArrayList()); 21 | // } 22 | // p.setMembers(new ArrayList()); 23 | // }); 24 | // } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | @SuppressWarnings("serial") 4 | public class UserNotFoundException extends RuntimeException { 5 | 6 | public UserNotFoundException(final Integer id) { 7 | super("Could not find User with id: " + id); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import java.util.Optional; 4 | 5 | import com.dlizarra.starter.support.jpa.CustomJpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface UserRepository extends CustomJpaRepository { 10 | 11 | Optional findByUsername(String username); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import java.util.List; 4 | 5 | import com.dlizarra.starter.role.RoleName; 6 | 7 | public interface UserService { 8 | 9 | void createUser(UserDto user, RoleName roleName); 10 | 11 | void updateUser(UserDto user); 12 | 13 | void deleteUser(Integer id); 14 | 15 | UserDto getUser(Integer id); 16 | 17 | List getUsers(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dlizarra/starter/user/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.domain.Sort; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import com.dlizarra.starter.role.RoleName; 13 | import com.dlizarra.starter.role.Role; 14 | import com.dlizarra.starter.support.orika.OrikaBeanMapper; 15 | 16 | @Service 17 | public class UserServiceImpl implements UserService { 18 | 19 | @Autowired 20 | private UserRepository userRepository; 21 | 22 | @Autowired 23 | private OrikaBeanMapper mapper; 24 | 25 | @Transactional 26 | @Override 27 | public void createUser(final UserDto userDto, final RoleName roleName) { 28 | final User user = mapper.map(userDto, User.class); 29 | final Role role = new Role(); 30 | role.setRolename(roleName); 31 | role.setUser(user); 32 | user.getRoles().add(role); 33 | user.setEnabled(true); 34 | 35 | userRepository.save(user); 36 | } 37 | 38 | @Transactional(readOnly = true) 39 | @Override 40 | public List getUsers() { 41 | final List users = userRepository.findAll(new Sort("id")); 42 | final List usersDto = new ArrayList(); 43 | users.forEach(x -> usersDto.add(mapper.map(x, UserDto.class))); 44 | 45 | return usersDto; 46 | } 47 | 48 | @Transactional(readOnly = true) 49 | @Override 50 | public UserDto getUser(final Integer id) { 51 | return mapper.map(find(id), UserDto.class); 52 | } 53 | 54 | @Transactional 55 | @Override 56 | public void updateUser(final UserDto user) { 57 | // TODO Auto-generated method stub 58 | 59 | } 60 | 61 | @Transactional 62 | @Override 63 | public void deleteUser(final Integer id) { 64 | userRepository.delete(id); 65 | } 66 | 67 | @Transactional(readOnly = true) 68 | private User find(final Integer id) { 69 | final Optional userOpt = userRepository.findOne(id); 70 | return userOpt.orElseThrow(() -> new UserNotFoundException(id)); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/resources/application-default.properties: -------------------------------------------------------------------------------- 1 | # H2 Embedded database configuration 2 | h2.jdbcurl=jdbc:h2:mem:embedded;DATABASE_TO_UPPER=false;MODE=PostgreSQL;DB_CLOSE_ON_EXIT=FALSE";DB_CLOSE_DELAY=-1 3 | h2.username=h2 4 | spring.h2.console.enabled=true -------------------------------------------------------------------------------- /src/main/resources/application-production.properties: -------------------------------------------------------------------------------- 1 | psql.jdbcurl= 2 | psql.username= 3 | spring.jpa.hibernate.ddl-auto=none -------------------------------------------------------------------------------- /src/main/resources/application-staging.properties: -------------------------------------------------------------------------------- 1 | psql.jdbcurl=jdbc:postgresql://localhost:5432/starterdatabase?currentSchema=starterschema 2 | psql.username=dlizarra 3 | spring.jpa.hibernate.ddl-auto=none -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #spring: 2 | #profiles.active: staging -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | -- users 2 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (1, 'david', '$2a$10$.ysnHr4PeaEgfljWaHexYO41hvcqmxLFOG69179iOLkHUKXRFpXKu', 1); 3 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (2, 'mark', '$2a$10$QIWJYadawFt4QQut5MRgdeSMQKFPROQELPWphpGgHYQl3VwLsqcgS', 1); 4 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (3, 'john', '$2a$10$LUVfN36xEPS4kqD7NNUuUemaI30J6wzYpkYN6X7UzYhpDun6vaLFC', 1); 5 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (4, 'ryan', '$2a$10$RwAaoqOzsS9J1RSivRozUeOj1Bs/uExeP1TMa6wG21zwll3Yp9DUC', 1); 6 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (5, 'martin', '$2a$10$ACRP9z0Ya//Nbym3oQj9Keq4NNXwoq.oyCnUlx1819RvlzLcqDTJq/uExeP1TMa6wG21zwll3Yp9DUC', 1); 7 | 8 | -- role 9 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (1, 'ROLE_ADMIN', 1); 10 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (2, 'ROLE_USER', 1); 11 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (3, 'ROLE_USER', 2); 12 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/java/com/dlizarra/starter/support/AbstractIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.IntegrationTest; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.ActiveProfiles; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | import com.dlizarra.starter.AppConfig; 10 | import com.dlizarra.starter.DatabaseConfig; 11 | import com.dlizarra.starter.SecurityConfig; 12 | import com.dlizarra.starter.StarterProfiles; 13 | 14 | @RunWith(SpringJUnit4ClassRunner.class) 15 | @SpringApplicationConfiguration(classes = { AppConfig.class, DatabaseConfig.class, SecurityConfig.class }) 16 | @IntegrationTest 17 | @ActiveProfiles(StarterProfiles.TEST) 18 | public abstract class AbstractIntegrationTest { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/dlizarra/starter/support/AbstractUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.SpringApplicationConfiguration; 5 | import org.springframework.test.context.ActiveProfiles; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | 8 | import com.dlizarra.starter.AppConfig; 9 | import com.dlizarra.starter.DatabaseConfig; 10 | import com.dlizarra.starter.SecurityConfig; 11 | import com.dlizarra.starter.StarterProfiles; 12 | 13 | @RunWith(SpringJUnit4ClassRunner.class) 14 | @SpringApplicationConfiguration(classes = { AppConfig.class, DatabaseConfig.class, SecurityConfig.class }) 15 | @ActiveProfiles(StarterProfiles.TEST) 16 | public abstract class AbstractUnitTest { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/dlizarra/starter/support/AbstractWebIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.support; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.SpringApplicationConfiguration; 5 | import org.springframework.boot.test.WebIntegrationTest; 6 | import org.springframework.test.context.ActiveProfiles; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | import com.dlizarra.starter.AppConfig; 10 | import com.dlizarra.starter.DatabaseConfig; 11 | import com.dlizarra.starter.SecurityConfig; 12 | import com.dlizarra.starter.StarterProfiles; 13 | 14 | @RunWith(SpringJUnit4ClassRunner.class) 15 | @SpringApplicationConfiguration(classes = { AppConfig.class, DatabaseConfig.class, SecurityConfig.class }) 16 | @WebIntegrationTest 17 | @ActiveProfiles(StarterProfiles.TEST) 18 | public abstract class AbstractWebIntegrationTest { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/dlizarra/starter/user/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.jdbc.Sql; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import com.dlizarra.starter.support.AbstractWebIntegrationTest; 14 | 15 | @Sql({ "classpath:/sql/cleanup.sql", "classpath:/sql/user.sql" }) 16 | public class UserRepositoryTest extends AbstractWebIntegrationTest { 17 | 18 | @Autowired 19 | private UserRepository userRepository; 20 | 21 | @Test 22 | public void save_UserGiven_ShouldSaveUser() { 23 | // arrange 24 | final User userStan = new User(); 25 | userStan.setUsername("stan"); 26 | userStan.setPassword("stanpass"); 27 | // act 28 | userRepository.save(userStan); 29 | // assert 30 | assertThat(userStan.getId()).isNotNull(); 31 | } 32 | 33 | @Test 34 | public void update_ExistingUserGiven_ShouldUpdateUser() { 35 | // arrange 36 | final User user = new User(); 37 | user.setId(2); 38 | user.setUsername("albert"); 39 | user.setPassword("albertpass"); 40 | // act 41 | final User updatedUser = userRepository.save(user); 42 | // assert 43 | assertThat(updatedUser).isEqualTo(user); 44 | } 45 | 46 | @Test 47 | public void findOne_ExistingIdGiven_ShouldReturnUser() { 48 | // act 49 | final Optional userOpt = userRepository.findOne(1); 50 | assertThat(userOpt.isPresent()).isTrue(); 51 | final User user = userOpt.get(); 52 | // assert 53 | assertThat(user.getUsername()).isEqualTo("david"); 54 | } 55 | 56 | @Transactional 57 | @Test 58 | public void getOne_ExistingIdGiven_ShouldReturnLazyEntity() { 59 | // act 60 | final User user1 = userRepository.getOne(1); 61 | // assert 62 | assertThat(user1).isNotNull(); 63 | assertThat(user1.getId()).isEqualTo(1); 64 | } 65 | 66 | @Sql({ "classpath:/sql/cleanup.sql", "classpath:/sql/user.sql" }) 67 | @Test 68 | public void findAll_TwoUsersinDb_ShouldReturnTwoUsers() { 69 | // act 70 | final List allUsers = userRepository.findAll(); 71 | // assert 72 | assertThat(allUsers.size()).isEqualTo(2); 73 | } 74 | 75 | @Sql({ "classpath:/sql/cleanup.sql", "classpath:/sql/user.sql" }) 76 | @Test 77 | public void delete_ExistingIdGiven_ShouldDeleteUser() { 78 | // act 79 | userRepository.delete(2); 80 | // assert 81 | assertThat(userRepository.findAll().size()).isEqualTo(1); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/dlizarra/starter/user/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.dlizarra.starter.user; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.mockito.Matchers.*; 5 | import static org.mockito.Mockito.*; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.MockitoAnnotations; 16 | import org.mockito.Spy; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.data.domain.Sort; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | import com.dlizarra.starter.support.AbstractUnitTest; 22 | import com.dlizarra.starter.support.orika.OrikaBeanMapper; 23 | 24 | @Transactional 25 | public class UserServiceTest extends AbstractUnitTest { 26 | 27 | @Mock 28 | private UserRepository userRepository; 29 | 30 | @Autowired 31 | @Spy 32 | private OrikaBeanMapper mapper; 33 | 34 | @InjectMocks 35 | private UserServiceImpl userService; 36 | 37 | @Before 38 | public void setup() { 39 | MockitoAnnotations.initMocks(this); 40 | final User u1 = new User(); 41 | u1.setId(1); 42 | u1.setUsername("david"); 43 | final User u2 = new User(); 44 | u2.setId(2); 45 | u2.setUsername("mark"); 46 | final List users = new ArrayList(); 47 | users.add(u1); 48 | users.add(u2); 49 | when(userRepository.findAll(any(Sort.class))).thenReturn(users); 50 | when(userRepository.findOne(1)).thenReturn(Optional.of(u1)); 51 | when(userRepository.findOne(500)).thenReturn(Optional.empty()); 52 | } 53 | 54 | @Test 55 | public void testFindAll_TwoUsersInDb_ShouldReturnTwoUsers() { 56 | // act 57 | final List users = userService.getUsers(); 58 | // assert 59 | assertThat(users.size()).isEqualTo(2); 60 | } 61 | 62 | @Test 63 | public void testGetUser_ExistingIdGiven_ShouldReturnUser() { 64 | // act 65 | final UserDto u = userService.getUser(1); 66 | // assert 67 | assertThat(u.getUsername()).isEqualTo("david"); 68 | } 69 | 70 | @Test(expected = UserNotFoundException.class) 71 | public void testGetUser_NonExistingIdGiven_ShouldThrowUserNotFoundException() { 72 | userService.getUser(500); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/resources/sql/cleanup.sql: -------------------------------------------------------------------------------- 1 | delete from "role"; 2 | delete from "users"; 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/sql/user.sql: -------------------------------------------------------------------------------- 1 | -- users 2 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (1, 'david', 'david', 1); 3 | INSERT INTO "users"("id","username", "password", "enabled") VALUES (2, 'mark', 'mark', 1); 4 | 5 | -- role 6 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (1, 'ROLE_ADMIN', 1); 7 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (2, 'ROLE_USER', 1); 8 | INSERT INTO "role"("id", "rolename", "user_id") VALUES (3, 'ROLE_USER', 2); 9 | --------------------------------------------------------------------------------