Spring boot ตอนที่ 3 – กรณีศึกษา MyFacebook web (3)

spring-boot-logo
ถ้าหากรอดจาก Spring security ในตอนที่แล้วมาได้ถัดจากนี้ก็สบายแล้วครับ บทนี้จะเป็นเรื่องเบาๆ ด้วยการการทำโพสต์และคอมเม้นต์โดยใช้ Spring data JPA


updated#1
รวมลิงก์ของทุกตอนครับ

updated#2
ตอนนี้ผมกำลังทำ video ลง youtube อยู่ครับ

  1. ปรับ SecurityConfig เวลา logout เสร็จให้กลับไปหน้า login

    package com.magicalcyber.myfacebook;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    import com.magicalcyber.myfacebook.service.UserDetailsServiceImpl;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    	@Override
    	public void configure(WebSecurity web) throws Exception {
    		web.ignoring().antMatchers("/css/**", "/img/**");
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.authorizeRequests().antMatchers("/login", "/", "/register").permitAll().anyRequest().authenticated()
    			.and()
    				.formLogin()
    				.loginPage("/login")
    				.defaultSuccessUrl("/")
    			.and()
    				.logout().logoutUrl("/logout")
    				.logoutSuccessUrl("/login");
    	}
    
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth.userDetailsService(userDetailService()).passwordEncoder(passwordEncoder());
    	}
    
    	@Bean
    	public PasswordEncoder passwordEncoder() {
    		return new BCryptPasswordEncoder();
    	}
    
    	@Bean
    	public UserDetailsService userDetailService() {
    		return new UserDetailsServiceImpl();
    	}
    
    }
    
    
  2. สร้าง model มาสองตัวเพื่อเก็บ Post และ Comment

    Post.java

    package com.magicalcyber.myfacebook.model;
    
    import java.sql.Timestamp;
    import java.util.Set;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.ManyToOne;
    import javax.persistence.OneToMany;
    import javax.persistence.OrderBy;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "post")
    public class Post {
    
    	@Id
    	@GeneratedValue
    	private Long id;
    
    	@Column
    	private String content;
    
    	@Column
    	private Timestamp createdDate;
    	
    	@ManyToOne
    	private User createdUser;
    
    	@OneToMany(cascade = CascadeType.ALL)
    	@OrderBy("createdDate")
    	private Set<Comment> comments;
    
    	public Long getId() {
    		return id;
    	}
    
    	public void setId(Long id) {
    		this.id = id;
    	}
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    
    	public Timestamp getCreatedDate() {
    		return createdDate;
    	}
    
    	public void setCreatedDate(Timestamp createdDate) {
    		this.createdDate = createdDate;
    	}
    
    	public Set<Comment> getComments() {
    		return comments;
    	}
    
    	public void setComments(Set<Comment> comments) {
    		this.comments = comments;
    	}
    
    	public User getCreatedUser() {
    		return createdUser;
    	}
    
    	public void setCreatedUser(User createdUser) {
    		this.createdUser = createdUser;
    	}
    
    	
    }
    
    

    Comment.java

    package com.magicalcyber.myfacebook.model;
    
    import java.sql.Timestamp;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.ManyToOne;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "comment")
    public class Comment {
    
    	@Id
    	@GeneratedValue
    	private Long id;
    
    	@Column
    	private String content;
    
    	@Column
    	private Timestamp createdDate;
    
    	@ManyToOne
    	private User createdUser;
    
    	public Long getId() {
    		return id;
    	}
    
    	public void setId(Long id) {
    		this.id = id;
    	}
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    
    	public Timestamp getCreatedDate() {
    		return createdDate;
    	}
    
    	public void setCreatedDate(Timestamp createdDate) {
    		this.createdDate = createdDate;
    	}
    
    	public User getCreatedUser() {
    		return createdUser;
    	}
    
    	public void setCreatedUser(User createdUser) {
    		this.createdUser = createdUser;
    	}
    
    }
    
    
  3. แก้ไข CustomUserDetail ให้รับค่า user id เข้ามาด้วย เนื่องจากในบทนี้จำเป็นต้องใช้
    package com.magicalcyber.myfacebook.model;
    
    import java.util.Collection;
    
    import org.springframework.security.core.GrantedAuthority;
    
    public class CustomUserDetail extends org.springframework.security.core.userdetails.User {
    
    	private static final long serialVersionUID = 1L;
    
    	private Long id;
    	private String fullname;
    
    	public CustomUserDetail(String username, String password, boolean enabled, boolean accountNonExpired,
    			boolean credentialsNonExpired, boolean accountNonLocked,
    			Collection<? extends GrantedAuthority> authorities) {
    		super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    	}
    
    	public CustomUserDetail(Long userId, String username, String password,
    			Collection<? extends GrantedAuthority> authorities, String fullname) {
    		super(username, password, authorities);
    		this.fullname = fullname;
    		this.id = userId;
    	}
    
    	public String getFullname() {
    		return fullname;
    	}
    
    	public void setFullname(String fullname) {
    		this.fullname = fullname;
    	}
    
    	public Long getId() {
    		return id;
    	}
    
    	public void setId(Long id) {
    		this.id = id;
    	}
    
    }
    
    

    แก้ UserDetailsServiceImpl เพื่อส่งค่า user id ดังนี้

    package com.magicalcyber.myfacebook.service;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.magicalcyber.myfacebook.model.CustomUserDetail;
    import com.magicalcyber.myfacebook.model.User;
    import com.magicalcyber.myfacebook.repository.UserRepository;
    
    @Service("userDetailService")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
    	@Autowired
    	private UserRepository userRepository;
    
    	@Override
    	@Transactional(readOnly = true)
    	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    		
    		User user = userRepository.findByEmail(email);
    
    		if (user == null) {
    			throw new UsernameNotFoundException(email);
    		}
    
    		Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
    		grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().getName()));
    
    		return new CustomUserDetail(user.getId(), email, user.getPassword(), grantedAuthorities, user.getName());
    	}
    
    }
    
    
  4. สร้างคลาส Form มาสองอันเพื่อรับค่าจากหน้าจอ
    PostForm.java

    package com.magicalcyber.myfacebook.web.form;
    
    import org.hibernate.validator.constraints.NotEmpty;
    
    public class PostForm {
    
    	private Long userId;
    
    	@NotEmpty
    	private String content;
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    
    	public Long getUserId() {
    		return userId;
    	}
    
    	public void setUserId(Long userId) {
    		this.userId = userId;
    	}
    
    	@Override
    	public String toString() {
    		return "PostForm [userId=" + userId + ", content=" + content + "]";
    	}
    
    	
    }
    
    

    CommentForm.java

    package com.magicalcyber.myfacebook.web.form;
    
    import org.hibernate.validator.constraints.NotEmpty;
    
    public class CommentForm {
    	private Long postId;
    	private Long userId;
    
    	@NotEmpty
    	private String content;
    
    	public Long getPostId() {
    		return postId;
    	}
    
    	public void setPostId(Long postId) {
    		this.postId = postId;
    	}
    
    	public Long getUserId() {
    		return userId;
    	}
    
    	public void setUserId(Long userId) {
    		this.userId = userId;
    	}
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    
    	@Override
    	public String toString() {
    		return "CommentForm [postId=" + postId + ", userId=" + userId + ", content=" + content + "]";
    	}
    
    }
    
    
  5. สร้าง interface PostRepository ขึ้นมาเพื่อดึง Post โดยเรียงลำดับจาก Post ล่าสุด
    package com.magicalcyber.myfacebook.repository;
    
    import java.util.List;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import com.magicalcyber.myfacebook.model.Post;
    import com.magicalcyber.myfacebook.model.User;
    
    public interface PostRepository extends JpaRepository<Post, Long> {
    	List<Post> findByCreatedUserOrderByCreatedDateDesc(User user);
    }
    
    

    ซึ่งทีเด็ดของ Spring data JPA เรื่อง naming convention ก็สามารถเอามาใช้ได้เหมือนกัน สังเกตชื่อ method ดีๆ จะพบว่า นอกจาก findBy[ชื่อฟิลด์] แล้วยังมี OrderBy[ชื่อฟิลด์] และเรียงลำดับ Desc ได้ด้วย

  6. สร้าง PostService และ Impl

    PostService.java

    package com.magicalcyber.myfacebook.service;
    
    import java.util.List;
    
    import com.magicalcyber.myfacebook.model.Post;
    import com.magicalcyber.myfacebook.web.form.CommentForm;
    import com.magicalcyber.myfacebook.web.form.PostForm;
    
    public interface PostService {
    	void save(PostForm postForm);
    
    	void saveComment(CommentForm commentForm);
    
    	List<Post> listPost(Long userId);
    }
    
    

    PostServiceImpl.java

    package com.magicalcyber.myfacebook.service;
    
    import java.sql.Timestamp;
    import java.util.HashSet;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.magicalcyber.myfacebook.model.Comment;
    import com.magicalcyber.myfacebook.model.Post;
    import com.magicalcyber.myfacebook.model.User;
    import com.magicalcyber.myfacebook.repository.PostRepository;
    import com.magicalcyber.myfacebook.repository.UserRepository;
    import com.magicalcyber.myfacebook.web.form.CommentForm;
    import com.magicalcyber.myfacebook.web.form.PostForm;
    
    @Service("postService")
    public class PostServiceImpl implements PostService {
    
    	@Autowired
    	private PostRepository postRepository;
    
    	@Autowired
    	private UserRepository userRepository;
    
    	@Override
    	public void save(PostForm postForm) {
    		User currentUser = userRepository.getOne(postForm.getUserId());
    		Post post = new Post();
    		post.setCreatedUser(currentUser);
    		post.setCreatedDate(new Timestamp(System.currentTimeMillis()));
    		post.setContent(postForm.getContent());
    		postRepository.save(post);
    	}
    
    	@Override
    	public List<Post> listPost(Long userId) {
    		return postRepository.findByCreatedUserOrderByCreatedDateDesc(userRepository.getOne(userId));
    	}
    
    	@Override
    	public void saveComment(CommentForm commentForm) {
    		Post post = postRepository.findOne(commentForm.getPostId());
    		if (post == null) {
    			throw new IllegalArgumentException("Not found this post");
    		} else {
    			if (post.getComments() == null) {
    				post.setComments(new HashSet<>());
    			}
    
    			User commentUser = userRepository.findOne(commentForm.getUserId());
    
    			Comment comment = new Comment();
    			comment.setCreatedUser(commentUser);
    			comment.setContent(commentForm.getContent());
    			comment.setCreatedDate(new Timestamp(System.currentTimeMillis()));
    
    			post.getComments().add(comment);
    			postRepository.save(post);
    		}
    	}
    
    }
    
    
  7. แก้ไขคลาส IndexController โดยตรวจสอบว่า
    • ถ้า User กำลัง login แล้วกดเมนู Home ก็ให้กลับเข้าหน้า Home พร้อมกับเตรียม model สำหรับการ Post และดึงข้อมูล Post ขึ้นมาแสดง
    • แต่ถ้าเป็นคนที่ยังไม่ได้ login ก็ให้ไปหน้า Register
    package com.magicalcyber.myfacebook.web.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AnonymousAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    import com.magicalcyber.myfacebook.model.CustomUserDetail;
    import com.magicalcyber.myfacebook.service.PostService;
    import com.magicalcyber.myfacebook.web.form.PostForm;
    import com.magicalcyber.myfacebook.web.form.RegisterForm;
    
    @Controller
    public class IndexController {
    
    	@Autowired
    	private PostService postService;
    
    	@GetMapping("/")
    	String index(Model model) {
    
    		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    		if (auth == null || (auth != null && auth instanceof AnonymousAuthenticationToken)) {
    			model.addAttribute("registerForm", new RegisterForm());
    			return "index";
    		} else {
    			// get current user
    			CustomUserDetail currentUser = (CustomUserDetail) auth.getPrincipal();
    
    			// create post model
    			PostForm form = new PostForm();
    			form.setUserId(currentUser.getId());
    			model.addAttribute("postForm", form);
    
    			// list post
    			model.addAttribute("posts", postService.listPost(currentUser.getId()));
    
    			return "hello";
    		}
    
    	}
    }
    
    
  8. ที่หน้า hello.jsp ผมแก้ไขยกเครื่องใหม่ดังนี้
    <%@ page language="java" contentType="text/html; charset=UTF-8"
    	pageEncoding="UTF-8"%>
    
    <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    <%@taglib prefix="sec"
    	uri="http://www.springframework.org/security/tags"%>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
    <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>My Facebook</title>
    <link rel="stylesheet" type="text/css"
    	href='<c:url value="/css/bootstrap.min.css" />' />
    <link rel="stylesheet" type="text/css"
    	href='<c:url value="/css/main.css" />' />
    </head>
    <body>
    	<%@include file="nav.jsp"%>
    
    
    	<div class="container">
    
    		<div class="row">
    			<div class="col-md-4 col-md-offset-4 block">
    				<form:form servletRelativeAction="/post" method="post"
    					modelAttribute="postForm">
    					<form:hidden path="userId" />
    					<div class="form-group">
    						<form:textarea path="content" cssClass="form-control" rows="3"
    							placeholder="คุณกำลังคิดอะไรอยู่?" />
    					</div>
    					<div class="text-right">
    						<input type="submit" class="btn btn-primary" value="Post"></input>
    					</div>
    				</form:form>
    			</div>
    		</div>
    
    
    		<c:if test="${ not empty posts }">
    			<div class="row">
    				<c:forEach items="${ posts }" var="post">
    					<div class="col-md-4 col-md-offset-4  block">
    						<div>
    							<strong>${ post.createdUser.name }</strong>
    							<p>
    								<fmt:formatDate value="${ post.createdDate }"
    									pattern="yyyy/MM/dd HH:mm" />
    							</p>
    						</div>
    						<hr />
    						<div>${ post.content }</div>
    
    						<c:choose>
    							<c:when test="${ not empty post.comments }">
    								<hr />
    								<div>
    									<strong>${ post.comments.size() } Comments</strong>
    								</div>
    								<br />
    								<c:forEach items="${ post.comments }" var="comment">
    									<div>
    										<strong>${ comment.createdUser.name }</strong>
    										<fmt:formatDate value="${ comment.createdDate }"
    											pattern="yyyy/MM/dd HH:mm" />
    									</div>
    									<div>${ comment.content }</div>
    									<hr />
    								</c:forEach>
    							</c:when>
    							<c:otherwise>
    								<hr />
    							</c:otherwise>
    						</c:choose>
    
    
    
    						<div>
    							<form:form cssClass="form-inline"
    								servletRelativeAction="/comment">
    								<div class="form-group">
    									<sec:authorize access="isAuthenticated()">
    										<sec:authentication var="user" property="principal" />
    										<input type="hidden" name="userId" value="${ user.id }" />
    									</sec:authorize>
    
    									<input type="hidden" name="postId" value="${ post.id }" />
    									<textarea class="form-control" rows="1" name="content"></textarea>
    									<input type="submit" class="btn btn-default" value="Post" />
    								</div>
    							</form:form>
    						</div>
    					</div>
    				</c:forEach>
    			</div>
    		</c:if>
    
    
    	</div>
    	<%@include file="footer.jsp"%>
    </body>
    </html>
    

    เพิ่ม main.css เพื่อสร้าง div ให้มองเป็น block ข้อความสีขาว

    body {
    	 background-color: #e9ebee;
    }
    
    .block {
    	background-color: #fff;
    	padding: 10px;
    	margin-bottom: 10px;
    }
    

    ส่วนของ footer.jsp

    <footer class="text-center" style="padding-top: 50px;">
    	MagicalCyber &copy; 2016 </footer>
    
  9. ส่วนของ nav.jsp ก็มีการแก้ไขเรื่อง
    • logout ให้แสดงเฉพาะ User ที่ login แล้วเท่านั้น
    • login ให้แสดงเฉพาะ User ที่ยังไม่ได้ login
    <%@ page language="java" contentType="text/html; charset=UTF-8"
    	pageEncoding="UTF-8"%>
    <%@taglib prefix="sec"
    	uri="http://www.springframework.org/security/tags"%>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
    <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta name="viewport"
    	content="width=device-width, initial-scale=1, maximum-scale=1" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>My Facebook</title>
    </head>
    <body>
    	<nav class="navbar navbar-default navbar-static-top">
    		<div class="container-fluid">
    
    			<!-- Brand and toggle get grouped for better mobile display -->
    			<div class="navbar-header">
    				<button type="button" class="navbar-toggle collapsed"
    					data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"
    					aria-expanded="false">
    					<span class="sr-only">Toggle navigation</span> <span
    						class="icon-bar"></span> <span class="icon-bar"></span> <span
    						class="icon-bar"></span>
    				</button>
    				<a class="navbar-brand" href="#">My Facebook</a>
    			</div>
    
    
    			<!-- Collect the nav links, forms, and other content for toggling -->
    			<div class="collapse navbar-collapse"
    				id="bs-example-navbar-collapse-1">
    				<ul class="nav navbar-nav">
    					<li class="active">
    						<a href='<c:url value="/" />'>Home <span class="sr-only">(current)</span></a>
    					</li>
    					<sec:authorize access="isAnonymous()">
    						<li>
    							<a href='<c:url value="/login" />'>Login</a>
    						</li>
    					</sec:authorize>
    					<sec:authorize access="isAuthenticated()">
    						<li>
    							<a href="#"> <sec:authorize access="isAuthenticated()">
    									<sec:authentication var="user" property="principal" />
    									<c:out value="${ user.fullname }"></c:out>
    
    								</sec:authorize>
    							</a>
    						</li>
    						<li>
    
    							<form:form cssClass="navbar-form" servletRelativeAction="/logout"
    								method="post">
    								<input type="submit" class="btn btn-danger" value="Logout" />
    							</form:form>
    						</li>
    					</sec:authorize>
    				</ul>
    			</div>
    
    		</div>
    	</nav>
    </body>
    </html>
    
  10. จากนั้น start spring boot และ login เข้าไป post ข้อความ ก็จะได้หน้าตาประมาณนี้

    1-post

Tests

ผมขอยกยอดไป test ในบทถัดไป เนื่องจากตอนนี้ผมยัง post เองแถมยัง comment เองอีก จึงรอตอนที่ทำ add friend แล้วมา comment โต้ตอบได้ก่อนครับ

Sourcecode

https://github.com/magickiat/myfacebook.git

Advertisements

5 thoughts on “Spring boot ตอนที่ 3 – กรณีศึกษา MyFacebook web (3)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s