├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ ├── java │ │ └── com │ │ │ └── reljicd │ │ │ ├── service │ │ │ ├── CommentService.java │ │ │ ├── UserService.java │ │ │ ├── PostService.java │ │ │ └── impl │ │ │ │ ├── CommentServiceImp.java │ │ │ │ ├── PostServiceImp.java │ │ │ │ └── UserServiceImp.java │ │ │ ├── repository │ │ │ ├── CommentRepository.java │ │ │ ├── RoleRepository.java │ │ │ ├── UserRepository.java │ │ │ └── PostRepository.java │ │ │ ├── BlogDemoApplication.java │ │ │ ├── controller │ │ │ ├── LoginController.java │ │ │ ├── BlogErrorController.java │ │ │ ├── HomeController.java │ │ │ ├── BlogController.java │ │ │ ├── RegistrationController.java │ │ │ ├── CommentController.java │ │ │ └── PostController.java │ │ │ ├── model │ │ │ ├── Role.java │ │ │ ├── Comment.java │ │ │ ├── Post.java │ │ │ └── User.java │ │ │ ├── util │ │ │ └── Pager.java │ │ │ └── config │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── MyAccessDeniedHandler.java │ │ │ └── SpringSecurityConfig.java │ └── resources │ │ ├── static │ │ └── css │ │ │ └── main.css │ │ ├── templates │ │ ├── home.html │ │ ├── error.html │ │ ├── 403.html │ │ ├── posts.html │ │ ├── fragments │ │ │ ├── posts.html │ │ │ ├── footer.html │ │ │ ├── pagination.html │ │ │ └── header.html │ │ ├── commentForm.html │ │ ├── login.html │ │ ├── post.html │ │ ├── postForm.html │ │ └── registration.html │ │ ├── application.properties │ │ └── sql │ │ └── import-h2.sql └── test │ └── java │ └── com │ └── reljicd │ └── BlogDemoApplicationTests.java ├── docker └── Dockerfile ├── .gitignore ├── scripts ├── run_docker.sh ├── mvnw.cmd └── mvnw ├── pom.xml ├── README.md └── LICENSE /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reljicd/spring-boot-blog/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.service; 2 | 3 | import com.reljicd.model.Comment; 4 | 5 | public interface CommentService { 6 | 7 | Comment save(Comment comment); 8 | } 9 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u151-jdk-alpine3.7 2 | 3 | EXPOSE 8090 4 | 5 | ENV APP_HOME /usr/src/app 6 | 7 | COPY target/blog-demo-0.0.1-SNAPSHOT.jar $APP_HOME/app.jar 8 | 9 | WORKDIR $APP_HOME 10 | 11 | ENTRYPOINT exec java -jar app.jar -------------------------------------------------------------------------------- /src/main/java/com/reljicd/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.repository; 2 | 3 | import com.reljicd.model.Comment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface CommentRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.service; 2 | 3 | import com.reljicd.model.User; 4 | 5 | import java.util.Optional; 6 | 7 | public interface UserService { 8 | 9 | Optional findByUsername(String username); 10 | 11 | Optional findByEmail(String email); 12 | 13 | User save(User user); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /src/main/java/com/reljicd/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.repository; 2 | 3 | import com.reljicd.model.Role; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | public interface RoleRepository extends JpaRepository { 8 | Role findByRole(@Param("role") String role); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/BlogDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.reljicd; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class BlogDemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(BlogDemoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/reljicd/BlogDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.reljicd; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class BlogDemoApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.repository; 2 | 3 | import com.reljicd.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserRepository extends JpaRepository { 10 | Optional findByEmail(@Param("email") String email); 11 | 12 | Optional findByUsername(@Param("username") String username); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/css/main.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #0000FF; 3 | } 4 | 5 | h2 { 6 | color: #FF0000; 7 | } 8 | 9 | footer { 10 | margin-top: 60px; 11 | } 12 | 13 | .validation-message { 14 | font-style: normal; 15 | font-size: 10px; 16 | color: #FF1C19; 17 | } 18 | 19 | a { 20 | transition: all .2s; 21 | } 22 | 23 | a:hover, a:focus, a:active { 24 | color: purple; 25 | text-decoration: none; 26 | } 27 | 28 | .pagination { 29 | display: block; 30 | text-align: center; 31 | } -------------------------------------------------------------------------------- /src/main/java/com/reljicd/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | import java.security.Principal; 7 | 8 | @Controller 9 | public class LoginController { 10 | 11 | @GetMapping("/login") 12 | public String login(Principal principal) { 13 | 14 | if (principal != null) { 15 | return "redirect:/home"; 16 | } 17 | return "/login"; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |

Error java.lang.NullPointerException

13 |

404 - Page does not exist

14 | Back to Home Page 15 | 16 |
17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/templates/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |
13 |

403 - Access is denied

14 |
Hello '[[${#httpServletRequest.remoteUser}]]', you do not have permission to access this page.
15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.repository; 2 | 3 | import com.reljicd.model.Post; 4 | import com.reljicd.model.User; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | 9 | import java.util.Optional; 10 | 11 | public interface PostRepository extends JpaRepository { 12 | Page findByUserOrderByCreateDateDesc(User user, Pageable pageable); 13 | 14 | Page findAllByOrderByCreateDateDesc(Pageable pageable); 15 | 16 | Optional findById(Long id); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/service/PostService.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.service; 2 | 3 | import com.reljicd.model.Post; 4 | import com.reljicd.model.User; 5 | import org.springframework.data.domain.Page; 6 | 7 | import java.util.Optional; 8 | 9 | public interface PostService { 10 | 11 | Optional findForId(Long id); 12 | 13 | Post save(Post post); 14 | 15 | /** 16 | * Finds a {@link Page) of {@link Post} of provided user ordered by date 17 | */ 18 | Page findByUserOrderedByDatePageable(User user, int page); 19 | 20 | /** 21 | * Finds a {@link Page) of all {@link Post} ordered by date 22 | */ 23 | Page findAllOrderedByDatePageable(int page); 24 | 25 | void delete(Post post); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/templates/posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |
13 |

Username Blog

14 |
15 | 16 |

17 |

18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/service/impl/CommentServiceImp.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.service.impl; 2 | 3 | import com.reljicd.model.Comment; 4 | import com.reljicd.repository.CommentRepository; 5 | import com.reljicd.service.CommentService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class CommentServiceImp implements CommentService { 11 | 12 | private final CommentRepository commentRepository; 13 | 14 | @Autowired 15 | public CommentServiceImp(CommentRepository commentRepository) { 16 | this.commentRepository = commentRepository; 17 | } 18 | 19 | @Override 20 | public Comment save(Comment comment) { 21 | return commentRepository.saveAndFlush(comment); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 |

Title

10 |
11 | Created by 12 |
13 |
User 14 |
15 |
16 |

Body

17 |

18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/controller/BlogErrorController.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.controller; 2 | 3 | import org.springframework.boot.autoconfigure.web.ErrorController; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | import org.springframework.web.servlet.ModelAndView; 8 | 9 | @RestController 10 | public class BlogErrorController implements ErrorController { 11 | 12 | private static final String PATH = "/error"; 13 | 14 | @RequestMapping(PATH) 15 | public ModelAndView error() { 16 | return new ModelAndView("/error"); 17 | } 18 | 19 | @GetMapping("/403") 20 | public ModelAndView error403() { 21 | return new ModelAndView("/403"); 22 | } 23 | 24 | @Override 25 | public String getErrorPath() { 26 | return PATH; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CONTAINER_NAME=spring-boot-blog-demo 4 | echo -e "\nSet docker container name as ${CONTAINER_NAME}\n" 5 | IMAGE_NAME=${CONTAINER_NAME}:dev 6 | echo -e "\nSet docker image name as ${IMAGE_NAME}\n" 7 | PORT=8090 8 | echo -e "Set docker image PORT to ${PORT}\n" 9 | 10 | echo -e "Create uber jar...\n" 11 | mvn clean package 12 | 13 | echo -e "\nStop running Docker containers with image tag ${CONTAINER_NAME}, and remove them...n" 14 | docker stop $(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}') 15 | docker rm $(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}') 16 | 17 | echo -e "\nDocker build image with name ${IMAGE_NAME}...\n" 18 | docker build -t ${IMAGE_NAME} -f docker/Dockerfile . 19 | 20 | echo -e "\nStart Docker container of the image ${IMAGE_NAME} with name ${CONTAINER_NAME}...\n" 21 | docker run --rm -i -p ${PORT}:${PORT} \ 22 | --name ${CONTAINER_NAME} \ 23 | ${IMAGE_NAME} -------------------------------------------------------------------------------- /src/main/java/com/reljicd/model/Role.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.model; 2 | 3 | import javax.persistence.*; 4 | import java.util.Collection; 5 | 6 | @Entity 7 | @Table(name = "role") 8 | public class Role { 9 | 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.AUTO) 12 | @Column(name = "role_id") 13 | private Long id; 14 | 15 | @Column(name = "role", unique = true) 16 | private String role; 17 | 18 | @ManyToMany(cascade = CascadeType.ALL, mappedBy = "roles") 19 | private Collection users; 20 | 21 | public Long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(Long id) { 26 | this.id = id; 27 | } 28 | 29 | public String getRole() { 30 | return role; 31 | } 32 | 33 | public void setRole(String role) { 34 | this.role = role; 35 | } 36 | 37 | public Collection getUsers() { 38 | return users; 39 | } 40 | 41 | public void setUsers(Collection users) { 42 | this.users = users; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/footer.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 | | Logged user: | 15 | Roles: | 16 | Sign Out 17 | 18 | 19 |
20 |

© 2017 Dusan Reljic

21 |
22 | 23 |
24 |
25 |
26 | 27 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/util/Pager.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.util; 2 | 3 | import com.reljicd.model.Post; 4 | import org.springframework.data.domain.Page; 5 | 6 | /** 7 | * @author Dusan Raljic 8 | */ 9 | public class Pager { 10 | 11 | private final Page posts; 12 | 13 | public Pager(Page posts) { 14 | this.posts = posts; 15 | } 16 | 17 | public int getPageIndex() { 18 | return posts.getNumber() + 1; 19 | } 20 | 21 | public int getPageSize() { 22 | return posts.getSize(); 23 | } 24 | 25 | public boolean hasNext() { 26 | return posts.hasNext(); 27 | } 28 | 29 | public boolean hasPrevious() { 30 | return posts.hasPrevious(); 31 | } 32 | 33 | public int getTotalPages() { 34 | return posts.getTotalPages(); 35 | } 36 | 37 | public long getTotalElements() { 38 | return posts.getTotalElements(); 39 | } 40 | 41 | public Page getPosts() { 42 | return posts; 43 | } 44 | 45 | public boolean indexOutOfBounds() { 46 | return getPageIndex() < 0 || getPageIndex() > getTotalElements(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.controller; 2 | 3 | import com.reljicd.model.Post; 4 | import com.reljicd.service.PostService; 5 | import com.reljicd.util.Pager; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | 13 | @Controller 14 | public class HomeController { 15 | 16 | private final PostService postService; 17 | 18 | @Autowired 19 | public HomeController(PostService postService) { 20 | this.postService = postService; 21 | } 22 | 23 | @GetMapping("/home") 24 | public String home(@RequestParam(defaultValue = "0") int page, 25 | Model model) { 26 | 27 | Page posts = postService.findAllOrderedByDatePageable(page); 28 | Pager pager = new Pager(posts); 29 | 30 | model.addAttribute("pager", pager); 31 | 32 | return "/home"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/reljicd/config/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.ResponseStatus; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | /** 12 | * Global exception handler 13 | */ 14 | @ControllerAdvice 15 | public class GlobalExceptionHandler { 16 | 17 | private static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); 18 | 19 | @ExceptionHandler(Throwable.class) 20 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 21 | public ModelAndView exception(final Throwable throwable) { 22 | 23 | logger.error("Exception during execution of SpringSecurity application", throwable); 24 | 25 | ModelAndView modelAndView = new ModelAndView("/error"); 26 | String errorMessage = (throwable != null ? throwable.toString() : "Unknown error"); 27 | modelAndView.addObject("errorMessage", errorMessage); 28 | return modelAndView; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #spring.profiles.active=production 2 | ################################################## 3 | server.port = 8090 4 | ################################################## 5 | # define H2 DataSrouce properties 6 | spring.datasource.url=jdbc:h2:mem:blog_simple_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 7 | spring.datasource.username=sa 8 | spring.datasource.password= 9 | #spring.datasource.driver-class-name=org.h2.Driver 10 | #spring.datasource.platform=h2 11 | spring.datasource.data=classpath:/sql/import-h2.sql 12 | ################################################## 13 | # enable H2 web console and set url for web console 14 | # http://localhost:8090/h2-console 15 | spring.h2.console.enabled=true 16 | spring.h2.console.path=/h2-console 17 | ################################################## 18 | # Spring Security 19 | # Queries for AuthenticationManagerBuilder 20 | spring.queries.users-query=select username, password, active from user where username=? 21 | spring.queries.roles-query=select u.username, r.role from user u inner join user_role ur on(u.user_id=ur.user_id) inner join role r on(ur.role_id=r.role_id) where u.username=? 22 | # Admin username and password 23 | spring.admin.username=admin 24 | spring.admin.password=admin 25 | ################################################## 26 | # Thymeleaf 27 | spring.thymeleaf.cache=false 28 | spring.thymeleaf.prefix=classpath:/templates -------------------------------------------------------------------------------- /src/main/java/com/reljicd/config/MyAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.reljicd.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.access.AccessDeniedException; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.web.access.AccessDeniedHandler; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | /** 16 | * Custom 403 access denied handler 17 | */ 18 | @Component 19 | public class MyAccessDeniedHandler implements AccessDeniedHandler { 20 | 21 | private static Logger logger = LoggerFactory.getLogger(MyAccessDeniedHandler.class); 22 | 23 | @Override 24 | public void handle(HttpServletRequest httpServletRequest, 25 | HttpServletResponse httpServletResponse, 26 | AccessDeniedException e) throws IOException { 27 | 28 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 29 | 30 | if (auth != null) { 31 | logger.info(String.format("User '%s' attempted to access the protected URL: %s", 32 | auth.getName(), httpServletRequest.getRequestURI())); 33 | } 34 | 35 | httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/403"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/pagination.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 35 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/commentForm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spring Boot Thymeleaf + Spring Security 5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 |
21 | 22 |

Write new comment

23 | 24 |
25 | 27 |