├── src
├── main
│ ├── frontend
│ │ ├── style
│ │ │ └── style.css
│ │ ├── .babelrc
│ │ ├── .eslintrc.js
│ │ ├── app
│ │ │ └── index.js
│ │ ├── package.json
│ │ └── webpack.config.js
│ ├── resources
│ │ ├── application.yml
│ │ ├── application-production.properties
│ │ ├── application-staging.properties
│ │ ├── application-default.properties
│ │ ├── static
│ │ │ └── index.html
│ │ └── data.sql
│ └── java
│ │ └── com
│ │ └── dlizarra
│ │ └── starter
│ │ ├── role
│ │ ├── RoleName.java
│ │ └── Role.java
│ │ ├── StarterMain.java
│ │ ├── user
│ │ ├── UserNotFoundException.java
│ │ ├── UserRepository.java
│ │ ├── UserService.java
│ │ ├── UserDtoMapper.java
│ │ ├── UserController.java
│ │ ├── UserDto.java
│ │ ├── UserServiceImpl.java
│ │ └── User.java
│ │ ├── StarterProfiles.java
│ │ ├── support
│ │ ├── security
│ │ │ ├── CurrentUser.java
│ │ │ ├── CustomUserDetailsService.java
│ │ │ └── CustomUserDetails.java
│ │ ├── jpa
│ │ │ ├── LocalDateTimeConverter.java
│ │ │ ├── ReadOnlyRepository.java
│ │ │ ├── CustomJpaRepository.java
│ │ │ └── CustomCrudRepository.java
│ │ └── orika
│ │ │ ├── LocalDateTimeConverter.java
│ │ │ └── OrikaBeanMapper.java
│ │ ├── AppConfig.java
│ │ ├── StarterApplication.java
│ │ ├── DatabaseConfig.java
│ │ └── SecurityConfig.java
└── test
│ ├── resources
│ └── sql
│ │ ├── cleanup.sql
│ │ └── user.sql
│ └── java
│ └── com
│ └── dlizarra
│ └── starter
│ ├── support
│ ├── AbstractUnitTest.java
│ ├── AbstractIntegrationTest.java
│ └── AbstractWebIntegrationTest.java
│ └── user
│ ├── UserServiceTest.java
│ └── UserRepositoryTest.java
├── Procfile
├── .travis.yml
├── .gitignore
├── pom.xml
└── README.md
/src/main/frontend/style/style.css:
--------------------------------------------------------------------------------
1 | .testblue {
2 | color: blue;
3 | }
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | #spring:
2 | #profiles.active: staging
--------------------------------------------------------------------------------
/src/main/frontend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-1"]
3 | }
--------------------------------------------------------------------------------
/src/test/resources/sql/cleanup.sql:
--------------------------------------------------------------------------------
1 | delete from "role";
2 | delete from "users";
3 |
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: java -Dspring.profiles.active=default -Dserver.port=$PORT -jar target/starter-0.0.1-SNAPSHOT.jar
2 |
--------------------------------------------------------------------------------
/src/main/resources/application-production.properties:
--------------------------------------------------------------------------------
1 | psql.jdbcurl=
2 | psql.username=
3 | spring.jpa.hibernate.ddl-auto=none
--------------------------------------------------------------------------------
/src/main/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "rules": {
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/main/resources/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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 extends GrantedAuthority> 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/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/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/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/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/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/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/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/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/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/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/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 extends T> entities);
93 |
94 | /**
95 | * Deletes all entities managed by the repository.
96 | */
97 | void deleteAll();
98 | }
99 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------