korean

Board program on Spring MVC

This section covers changing the Model 2 project to the Spring MVC project.

Prerequisites

You need the final source of the Building Spring MVC with Maven section and JSP Porject chapter.

The source of the JSP Project is also available at the following address:

git clone https://github.com/kimjonghoon/model2

The database design remains the same as the Model 2 database design. See Database design. See Database design.

Copy static files

Create a subdirectory named `resources` within the Document Base directory `C:/www/jwpsamples/src/main/webapp`, then copy the `css/` and `images/` directories from `C:/www/model2/WebContent` (of the Model 2 bulletin board) and paste them into the newly created `resources` directory.

Copy JSPs

Create the /views subdirectory under C:/www/jwpsamples/src/main/webapp/WEB-INF.
Copy the index.jsp, error.jsp, /bbs, /inc, /java, /users directory from the Document Base of Model 2 board and paste them into the /views directory. The reason for putting JSPs in the /WEB-INF directory is to prevent the web browser from directly accessing them.

Copy Java sources

Copy all the Java source directories from C:/www/JSPProject/src on the Model 2 bulletin board and paste them into C:/www/jwpsamples/src/main/java. Delete the action classes and the servlets that act as controllers.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
  version="6.1">

<display-name>Java Web Programming Samples</display-name>

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>

<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value></param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
	<multipart-config>
		<max-file-size>1048576</max-file-size>
		<max-request-size>1048576</max-request-size>
		<file-size-threshold>524288</file-size-threshold>
	</multipart-config>
</servlet>

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

<error-page>
	<error-code>403</error-code>
	<location>/WEB-INF/views/403.jsp</location>
</error-page>

<error-page>
	<error-code>404</error-code>
	<location>/WEB-INF/views/404.jsp</location>
</error-page>

<error-page>
	<error-code>500</error-code>
	<location>/WEB-INF/views/500.jsp</location>
</error-page>

<error-page>
	<exception-type>java.lang.Throwable</exception-type>
	<location>/WEB-INF/views/error.jsp</location>
</error-page>

</web-app>

The <multipart-config> element defines the file upload settings. A <max-request-size> value of 1,048,576 (1024 * 1024) limits the maximum size of an uploaded file to 1MB.

The <error-page> element configures the page to be displayed when an error occurs in the web application that is not caught by the application itself and is passed up to a WAS (Web Application Server) like Tomcat.

Remove '.do' from request strings within JSP source

The Model 2 board controller was only responsible for requests ending in .do. However, in Spring MVC, it is better to set the dispatcher servlet to handle all requests. So, you need to remove the '.do' from the request string ending in '.do' in the JSP. For example, modify ../users/login.do in header.jsp to ../users/login.

Spring XML Configuration

Create a Spring configuration file named mvc.xml in the WEB-INF/spring/ directory. (If you have completed the previous example, a file with the same name already exists in this location. Open the file and modify it by referring to the code below.)

mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc" 
	xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/mvc
		http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://mybatis.org/schema/mybatis-spring
		http://mybatis.org/schema/mybatis-spring.xsd">

<mvc:resources mapping="/resources/**" location="/resources/" />

<mvc:annotation-driven />

<context:component-scan base-package="net.java_school.controller" />

<mybatis:scan base-package="net.java_school.mapper" />

<bean id="internalResourceViewResolver" 
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
	<property name="prefix" value="/WEB-INF/views/" />
	<property name="suffix" value=".jsp" />
</bean>

<bean id="multipartResolver" 
	class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

<bean id="dataSource" 
		class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
	<property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
	<property name="username" value="java" />
	<property name="password" value="school" />
	<property name="maxTotal" value="100" />
	<property name="maxWaitMillis" value="1000" />
	<property name="poolPreparedStatements" value="true" />
	<property name="defaultAutoCommit" value="true" />
	<property name="validationQuery" value=" SELECT 1 FROM DUAL" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>

</beans>

pom.xml

The items to add to the pom.xml file are the log4j2 and the jetty-maven-plugin. You can run the Jetty server using the jetty-maven-plugin. The command to start the jetty server is mvn clean jetty:run. Before executing the command, you must stop Tomcat. Running the Jetty plugin prints the web application execution logs directly to the terminal, making it highly advantageous for error detection.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>net.java_school</groupId>
<artifactId>jwpsamples</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Java Web Programming Samples</name>
<url>http://localhost:8080</url>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<maven.compiler.source>25</maven.compiler.source>
	<maven.compiler.target>25</maven.compiler.target>
</properties>

<dependencyManagement>
	<dependencies>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-framework-bom -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-framework-bom</artifactId>
			<version>6.2.19</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>		
</dependencyManagement>

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.13.2</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webmvc</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-jdbc</artifactId>
	</dependency>
	<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc11 -->
	<dependency>
		<groupId>com.oracle.database.jdbc</groupId>
		<artifactId>ojdbc11</artifactId>
		<version>23.26.3.0.0</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis-spring</artifactId>
		<version>4.1.0</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis</artifactId>
		<version>3.6.0</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-dbcp2</artifactId>
		<version>2.14.0</version>
	</dependency>
	<!-- Logging -->
	<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
	<dependency>
		<groupId>org.apache.logging.log4j</groupId>
		<artifactId>log4j-api</artifactId>
		<version>2.26.1</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
	<dependency>
		<groupId>org.apache.logging.log4j</groupId>
		<artifactId>log4j-core</artifactId>
		<version>2.26.1</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jcl -->
	<dependency>
		<groupId>org.apache.logging.log4j</groupId>
		<artifactId>log4j-jcl</artifactId>
		<version>2.26.1</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
	<dependency>
		<groupId>jakarta.servlet</groupId>
		<artifactId>jakarta.servlet-api</artifactId>
		<version>6.1.0</version>
		<scope>provided</scope>
	</dependency>
	<!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api -->
	<dependency>
		<groupId>jakarta.servlet.jsp.jstl</groupId>
		<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
		<version>3.0.2</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl -->
	<dependency>
		<groupId>org.glassfish.web</groupId>
		<artifactId>jakarta.servlet.jsp.jstl</artifactId>
		<version>3.0.1</version>
	</dependency>
</dependencies>

<build>
	<finalName>ROOT</finalName>
	<pluginManagement>
		<plugins>
			<plugin>
				<artifactId>maven-clean-plugin</artifactId>
				<version>3.1.0</version>
				<configuration>
					<filesets>
						<fileset>
							<directory>src/main/webapp/WEB-INF/classes</directory>
						</fileset>
						<fileset>
							<directory>src/main/webapp/WEB-INF/lib</directory>
						</fileset>
					</filesets>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.0.2</version>
			</plugin>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.0</version>
			</plugin>
			<plugin>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.22.1</version>
			</plugin>
			<plugin>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.2.2</version>
			</plugin>
			<plugin>
				<artifactId>maven-install-plugin</artifactId>
				<version>2.5.2</version>
			</plugin>
			<plugin>
				<artifactId>maven-deploy-plugin</artifactId>
				<version>2.8.2</version>
			</plugin>
			<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-maven-plugin -->
			<plugin>
				<groupId>org.eclipse.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>11.0.26</version>
			</plugin>
		</plugins>
	</pluginManagement>
</build>
</project>

Logging

Copy the following commons-logging.properties and log4j2.xml from the Model 2 project and paste them into C:/www/jwpsamples/src/main/resources. If the resources directory does not exist, create it and run Maven - Update Project... to synchronize.

commons-logging.properties
org.apache.commons.logging.Log = org.apache.commons.logging.impl.Log4JLogger
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <File name="File" fileName="jwp.log" append="false">
      <PatternLayout pattern="%t %-5p %c{2} - %m%n" />
    </File>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
    </Console>
  </Appenders>
  <Loggers>
    <Logger name="net.java_school" level="DEBUG" additivity="false">
      <AppenderRef ref="File" />
    </Logger>
    <Root level="INFO">
      <AppenderRef ref="Console" />
    </Root>
  </Loggers>
</Configuration>

User

We will modify database-related code to use the MyBatis-Spring. Change UserService.java to be an interface.

UserService.java
package net.java_school.user;

public interface UserService {
    
  public void addUser(User user);

  public User login(String email, String passwd);

  public int editAccount(User user);

  public int changePasswd(String currentPasswd, String newPasswd, String email);

  public void bye(User user);

  public User getUser(String email);
}

Create UserMapper.java in the net.java_school.mapper package.

UserMapper.java
package net.java_school.mapper;

import org.apache.ibatis.annotations.Param;

import net.java_school.user.User;

public interface UserMapper {
    
  public void insert(User user);

  public User login(
    @Param("email") String email, 
    @Param("passwd") String passwd);

  public int update(User user);

  public int updatePasswd(
    @Param("currentPasswd") String currentPasswd, 
    @Param("newPasswd") String newPasswd, 
    @Param("email") String email);

  public int delete(User user);

  public User selectOne(String email);
}

Create UserServiceImpl.java in the net.java_school.user package.

UserServiceImpl.java
package net.java_school.user;

import net.java_school.mapper.UserMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    
  @Autowired
  private UserMapper userMapper;
    
  public void addUser(User user) {
    userMapper.insert(user);
  }

  public User login(String email, String passwd) {
    return userMapper.login(email, passwd);
  }

  public int editAccount(User user) {
    return userMapper.update(user);
  }

  public int changePasswd(String currentPasswd, String newPasswd, String email) {
    return userMapper.updatePasswd(currentPasswd, newPasswd, email);
  }

  public void bye(User user) {
    userMapper.delete(user);
  }

  public User getUser(String email) {
    return userMapper.selectOne(email);
  }
}

create the the MyBatis Configuration file in src/main/resources,

mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration 
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration> 

  <typeAliases>
    <typeAlias type="net.java_school.board.AttachFile" alias="AttachFile" />
    <typeAlias type="net.java_school.board.Comment" alias="Comment" />
    <typeAlias type="net.java_school.board.Board" alias="Board" />
    <typeAlias type="net.java_school.board.Article" alias="Article" />
    <typeAlias type="net.java_school.user.User" alias="User" />
  </typeAliases>

</configuration>

Create the UserMapper.xml file in the src/main/resources/net/java_school/mapper directory so that it shares the same classpath as the UserMapper interface. In the UserMapper.xml file, ensure that the id values match the method names defined in UserMapper.java.

UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="net.java_school.mapper.UserMapper">
    
  <insert id="insert" parameterType="User">
    INSERT INTO member VALUES (#{email}, #{passwd}, #{name}, #{mobile})
  </insert>

  <select id="login" resultType="User">
    SELECT email, passwd, name, mobile FROM member 
    WHERE email = #{email} AND passwd = #{passwd}
  </select>

  <update id="update" parameterType="User">
    UPDATE member SET name = #{name}, mobile = #{mobile} 
    WHERE email = #{email} AND passwd = #{passwd}
  </update>

  <update id="updatePasswd">
    UPDATE member SET passwd = #{newPasswd} 
    WHERE passwd = #{currentPasswd} AND email = #{email}
  </update>

  <delete id="delete">
    DELETE FROM member 
    WHERE email = #{email}
  </delete>

  <select id="selectOne" parameterType="string" resultType="User">
    SELECT email, passwd, name, mobile 
    FROM member
    WHERE email = #{email}
  </select>
</mapper>

Create a UsersController class in the net.java_school.controller package to handle all requests containing "/users".

UsersController.java
package net.java_school.controller;

import java.net.URLEncoder;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

import net.java_school.commons.WebContants;
import net.java_school.exception.AuthenticationException;
import net.java_school.user.User;
import net.java_school.user.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("users")
public class UsersController {
    
  @Autowired
  private UserService userService;

  @GetMapping("signUp")
  public String signUp() {
    return "users/signUp";
  }

  @PostMapping("signUp")
  public String signUp(User user) {
    userService.addUser(user);
    return "redirect:/users/welcome";
  }

  @GetMapping("welcome")
  public String welcome() {
    return "users/welcome";
  }

  @GetMapping("login")
  public String login() {
    return "users/login";
  }
    
  @PostMapping("login")
  public String login(String email, String passwd, String url, HttpSession session) {
    User user = userService.login(email, passwd);
        
    if (user == null) {
      return "redirect:/users/login?url=" + url + "&msg=Login-Failed";
    } else {
      session.setAttribute(WebContants.USER_KEY, user);
      if (!url.equals("")) {
        return "redirect:" + url;
      }
      
      return "redirect:/";
    }
  }
        
  @GetMapping("editAccount")
  public String editAccount(HttpServletRequest req, HttpSession session) throws Exception {
    User user = (User) session.getAttribute(WebContants.USER_KEY);

    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      
      return "redirect:/users/login?url=" + url;
    }

    return "users/editAccount";
  }
    
  @PostMapping("editAccount")
  public String editAccount(User user, HttpSession session) {
    User loginUser = (User) session.getAttribute(WebContants.USER_KEY);
    if (loginUser == null) {
      throw new AuthenticationException(WebContants.NOT_LOGIN);
    }

    user.setEmail(loginUser.getEmail());
        
    int check = userService.editAccount(user);
    if (check < 1) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }
    
    session.setAttribute(WebContants.USER_KEY, user);

    return "users/changePasswd";
  }
    
  @GetMapping("changePasswd")
  public String changePasswd(HttpServletRequest req, HttpSession session) throws Exception {
    User user = (User) session.getAttribute(WebContants.USER_KEY);
        
    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      return "redirect:/users/login?url=" + url;     
    }
        
    return "users/changePasswd";
  }
    
  @PostMapping("changePasswd")
  public String changePasswd(String currentPasswd, String newPasswd, HttpSession session) {
    String email = ((User)session.getAttribute(WebContants.USER_KEY)).getEmail();
        
    int check = userService.changePasswd(currentPasswd, newPasswd, email);
    if (check < 1) {
        throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    } 
    
    return "redirect:/users/changePasswd_confirm";
  }
    
  @GetMapping("changePasswd_confirm")
  public String changePasswd_confirm() {
    return "users/changePasswd_confirm";
  }
    
  @GetMapping("bye")
  public String bye(HttpServletRequest req, HttpSession session) throws Exception {
    User user = (User)session.getAttribute(WebContants.USER_KEY);
        
    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      
      return "redirect:/users/login?url=" + url;     
    }
        
    return "users/bye";
  }

  @PostMapping("bye")
  public String bye(String email, String passwd, HttpSession session) {
    User user = (User)session.getAttribute(WebContants.USER_KEY);

    if (user == null || !user.getEmail().equals(email)) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }
    
    user = userService.login(email, passwd);
    userService.bye(user);
    session.removeAttribute(WebContants.USER_KEY);
    
    return "redirect:/users/bye_confirm";
  }
  
  @GetMapping("bye_confirm")
  public String bye_confirm() {
    return "users/bye_confirm";    
  } 
    
  @GetMapping("logout")
  public String logout(HttpSession session) {
    session.removeAttribute(WebContants.USER_KEY);
    return "redirect:/";
  }
}

@GetMapping("signUp") annotation above the method declaration allows the method to handle the "/users/signUp" request of the GET method. (The "/users" part of "/users/signUp" is due to the @RequestMapping("users") annotation added above the UsersController class declaration)

@PostMapping("/signUp") above the method declaration allows the method to handle the "/users/signUp" request of the POST method.

Even if the request string is the same, it is possible to handle each request separately by HTTP method.

The method invoked on the /users/signUp request of the POST method gets your web browser to redirect the request to welcome.jsp after completing membership. If this method changes the screen using forwarding after membership, the user can subscribe with the same information using the F5 button. Redirect does not have this problem. If you have chosen a redirect, You need to write a method with the following annotation on the controller.

@GetMapping("welcome")

HomeController is the controller responsible for homepage (/) request.

HomeController.java
package net.java_school.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

  @GetMapping("/")
  public String index() {
    return "index";
  }
}

JavaController is the controller that handles requests starting with /java.

JavaController.java
package net.java_school.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GettMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("java")
public class JavaController {

  @GetMapping
  public String index() {
    return "java/index";
  }
    
  @GetMapping("jdk-install")
  public String basic() {
    return "java/jdk-install";
  }
}

JavascriptController is the controller that handles requests starting with /javascript.

JavascriptController.java
package net.java_school.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("javascript")
public class JavascriptController {

  @GetMapping
  public String index() {
    return "javascript/index";
  }
}
mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:p="http://www.springframework.org/schema/p"
  xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://mybatis.org/schema/mybatis-spring 
    http://mybatis.org/schema/mybatis-spring.xsd">
            
  <!-- The following tells Dispatcher servlet the location of the static resource.  -->
  <mvc:resources mapping="/resources/**" location="/resources/" />
        
  <mvc:annotation-driven />
    
  <context:component-scan
    base-package="net.java_school.controller,
      net.java_school.board, 
      net.java_school.user" />

  <mybatis:scan base-package="net.java_school.mapper" />

  <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
    <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:XE"/>
    <property name="username" value="java"/>
    <property name="password" value="school"/>
    <property name="maxTotal" value="100"/>
    <property name="maxWaitMillis" value="1000"/>
    <property name="poolPreparedStatements" value="true"/>
    <property name="defaultAutoCommit" value="true"/>
    <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
  </bean>
    
  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis-config.xml" />
  </bean>

  <bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />
    
  <!-- ViewResolver -->
  <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
  </bean>
  
  <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="defaultErrorView" value="error" />
    <property name="exceptionMappings">
      <props>
        <prop key="net.java_school.exception.AuthenticationException">
          403-error
        </prop>
      </props>
    </property>
  </bean>
</beans>
<mvc:resources location=".." />

It tells the dispatcher servlet the location of static resources.

<context:component-scan base-package=".." />

It let Spring scan Java beans and register them in the container. You don't need to set beans that Spring auto-scans in your Spring configuration file. Beans to be auto-scanned must-have component annotations above the class declaration and belong to the base package. Spring refers to the annotations above the class declaration when it scans beans. For example, if @Repository, Spring treats this class as Dao. But if @Componenct or @Service, Spring scans these classes but does not treat them as special. Spring applications that run on an annotation base require the following settings:

<mvc:annotation-driven />
<mybatis:scan base-package=".." />

It let Spring scan mybatis-spring beans and register them in the container. Using the above configuration, you can omit the following from the Spring configuration file.

<bean id="boardMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="net.java_school.mapper.BoardMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<bean id="dataSource" ..>

You need to the dataSource definition whether you use Spring JDBC or MyBatis.

<bean id="sqlSessionFactory" ..>

MyBatis applications use instances of the SqlSessionFactory interface type.

<bean id="multipartResolver" ..>

When uploading a file, Spring passes a wrapper for the HttpServletRequest to the controller. The method that handles the uploaded file cast the wrapper to the MultiPartHttpServletRequest interface to control the multipart file passed to the server.

The following is a setting for how to interpret the view from the string returned by the controller.

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

It is a Spring MVC exception handling setting. Spring MVC has several exception handling methods. Among them, it is easiest to use SimpleMappingExceptionResolver.

<prop key="net.java_school.exception.AuthenticationException">
  403-error
</prop>

According to the above, when an AuthenticationException exception occurs, the 403-error view is selected. -- Our view resolver will interpret the 403-error as /WEB-INF/views/403-error.jsp --

Create a 403-error.jsp file that says "Access Denied." -- You do not need to define a handler for the /403-error to the controller --

<property name="defaultErrorView" value="error" />

According to the above, when an exception other than AuthenticationException occurs, the error view is selected. -- Our view resolver will interpret the error as /WEB-INF/views/error.jsp --

Board

Create BoardMapper.xml in the exact location as the UserMapper.xml, as configured in mybatis-config.xml.

BoardMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="net.java_school.mapper.BoardMapper">

  <select id="selectListOfArticles" parameterType="hashmap" resultType="Article">
    SELECT articleno, title, regdate, hit, name, attachfileNum, commentNum 
    FROM (
      SELECT rownum r,a.* 
        FROM (
          SELECT 
            a.articleno, a.title, a.regdate, a.hit, m.name,
            COUNT(DISTINCT(f.attachfileno)) attachfileNum, 
            COUNT(DISTINCT(c.commentno)) commentNum
          FROM 
            article a, attachfile f, comments c, member m
          WHERE
            a.articleno = f.articleno(+)
            AND a.articleno = c.articleno(+)
            AND a.email = m.email(+)
            AND a.boardcd = #{boardCd}
            <if test="search != null and search != ''">
            AND (title LIKE '%${search}%' OR DBMS_LOB.INSTR(content, #{search}) > 0)
            </if>
          GROUP BY a.articleno, title, a.regdate, hit, m.name
          ORDER BY articleno DESC
        ) a
    )
    WHERE r BETWEEN #{start} AND #{end}
    </select>

  <select id="selectCountOfArticles" parameterType="hashmap" resultType="int">
    SELECT count(*) FROM article WHERE boardcd = #{boardCd}
    <if test="search != null and search != ''">
    AND (title LIKE '%${search}%' OR DBMS_LOB.INSTR(content, #{search}) > 0)
    </if>
  </select>

  <insert id="insert" parameterType="Article" useGeneratedKeys="true">
    <selectKey keyProperty="articleNo" resultType="int" order="BEFORE">
      SELECT seq_article.nextval FROM dual
    </selectKey>
    INSERT INTO article (articleNo, boardCd, title, content, email, hit, regdate)
    VALUES
    (#{articleNo}, #{boardCd}, #{title}, #{content}, #{email}, 0, sysdate)
  </insert>

  <insert id="insertAttachFile" parameterType="AttachFile">
    INSERT INTO attachfile (attachfileno, filename, filetype, filesize, articleno, email)
    VALUES
    (seq_attachfile.nextval, #{filename}, #{filetype}, #{filesize}, #{articleNo}, #{email})
  </insert>
    
  <update id="update" parameterType="Article">
    UPDATE article 
    SET title = #{title}, content = #{content} 
    WHERE articleno = #{articleNo}
  </update>
    
  <delete id="delete" parameterType="int">
    DELETE FROM article WHERE articleno = #{articleNo}
  </delete>
    
  <update id="updateHitPlusOne" parameterType="int">
    UPDATE article SET hit = hit + 1 WHERE articleno = #{articleNo}
  </update>
    
  <select id="selectOne" parameterType="int" resultType="Article">
    SELECT 
      articleno, 
      title, 
      content, 
      a.email, 
      NVL(name, 'Anonymous') name, 
      hit, 
      regdate
    FROM article a, member m
      WHERE a.email = m.email(+) AND articleno = #{articleNo}
  </select>
    
  <select id="selectNextOne" parameterType="hashmap" resultType="Article">
    SELECT articleno, title
    FROM
    (SELECT rownum r,a.*
      FROM
        (SELECT articleno, title 
        FROM article 
        WHERE 
          boardCd = #{boardCd} 
          AND articleno > #{articleNo}
          <if test="search != null and search != ''">
          AND (title LIKE '%${search}%' OR DBMS_LOB.INSTR(content, #{search}) > 0)
          </if>
        ORDER BY articleno) 
    a)
    WHERE r = 1
  </select>
    
  <select id="selectPrevOne" parameterType="hashmap" resultType="Article">
    SELECT articleno, title
    FROM
      (SELECT rownum r,a.*
      FROM
        (SELECT articleno, title 
        FROM article 
        WHERE 
          boardCd = #{boardCd} 
          AND articleno < #{articleNo}
          <if test="search != null and search != ''">
          AND (title LIKE '%${search}%' OR DBMS_LOB.INSTR(content, #{search}) > 0)
          </if> 
        ORDER BY articleno DESC)
      a)
    WHERE r = 1
  </select>
    
  <select id="selectListOfAttachFiles" parameterType="int" resultType="AttachFile">
    SELECT 
      attachfileno, 
      filename, 
      filetype, 
      filesize, 
      articleno, 
      email
    FROM attachfile 
    WHERE articleno = #{articleNo} 
    ORDER BY attachfileno
  </select>
    
  <delete id="deleteFile" parameterType="int">
    DELETE FROM attachfile WHERE attachfileno = #{attachFileNo}
  </delete>
    
  <select id="selectOneBoard" parameterType="string" resultType="string">
    SELECT * FROM board WHERE boardcd = #{boardCd}
  </select>
    
  <insert id="insertComment" parameterType="Comment">
    INSERT INTO comments (commentno, articleno, email, memo, regdate)
    VALUES (seq_comments.nextval, #{articleNo}, #{email}, #{memo}, sysdate)
  </insert>
    
  <update id="updateComment" parameterType="Comment">
    UPDATE comments SET memo = #{memo} WHERE commentno = #{commentNo}
  </update>
    
  <delete id="deleteComment" parameterType="int">
    DELETE FROM comments WHERE commentno = #{commentNo}
  </delete>

  <select id="selectListOfComments" parameterType="int" resultType="Comment">
    SELECT 
      commentno, 
      articleno, 
      c.email, 
      NVL(name,'Anonymous') name, 
      memo, 
      regdate
    FROM comments c, member m
    WHERE 
      c.email = m.email(+)
      AND articleno = #{articleNo}
    ORDER BY commentno DESC
  </select>

  <select id="selectOneAttachFile" parameterType="int" resultType="AttachFile">
    SELECT 
      attachfileno, 
      filename, 
      filetype, 
      filesize, 
      articleno, 
      email
    FROM attachfile
    WHERE attachfileno = #{attachFileNo}
  </select>
    
  <select id="selectOneComment" parameterType="int" resultType="Comment">
    SELECT 
      commentno,
      articleno,
      email,
      memo,
      regdate 
    FROM comments 
    WHERE commentno = #{commentNo}
  </select>
</mapper>

To make MyBatis return a unique number after an insert, add the useGeneratedKeys="true" attribute. For databases that do not have auto-increment columns like Oracle, add the selectKey subelement.

<insert id="insert" parameterType="Article" useGeneratedKeys="true">
  <selectKey keyProperty="articleNo" resultType="int" order="BEFORE">
    SELECT seq_article.nextval FROM dual
  </selectKey>
  INSERT INTO article (articleNo, boardCd, title, content, email, hit, regdate)
  VALUES
  (#{articleNo}, #{boardCd}, #{title}, #{content}, #{email}, 0, sysdate)
</insert>

Create BoardMapper.java like below.

BoardMapper.java
package net.java_school.mapper;

import java.util.HashMap;
import java.util.List;

import net.java_school.board.Article;
import net.java_school.board.AttachFile;
import net.java_school.board.Comment;

public interface BoardMapper {

  public List<Article> selectListOfArticles(HashMap<String, String> hashmap);  

  public int selectCountOfArticles(HashMap<String, String> hashmap);

  public int insert(Article article);   

  public void insertAttachFile(AttachFile attachFile);

  public void update(Article article);  

  public void delete(int articleNo);

  public void updateHitPlusOne(int articleNo);  

  public Article selectOne(int articleNo);

  public Article selectNextOne(HashMap<String, String> hashmap); 

  public Article selectPrevOne(HashMap<String, String> hashmap);

  public List<AttachFile> selectListOfAttachFiles(int articleNo);    

  public void deleteFile(int attachFileNo); 

  public String selectOneBoard(String boardCd);

  public void insertComment(Comment comment);   

  public void updateComment(Comment comment);

  public void deleteComment(int commentNo);

  public List<Comment> selectListOfComments(int articleNo);

  public AttachFile selectOneAttachFile(int attachFileNo);

  public Comment selectOneComment(int commentNo);
} 

Modify BoardService.java to be an interface.

BoardService.java
package net.java_school.board;

import java.util.List;

public interface BoardService {

  public List<Article> getArticleList(String boardCd, String search, Integer startRecord, Integer endRecord);

  public int getTotalRecord(String boardCd, String search);

  public int addArticle(Article article);

  public void addAttachFile(AttachFile attachFile);

  public void modifyArticle(Article article);

  public void removeArticle(int articleNo);

  public void increaseHit(int articleNo);

  public Article getArticle(int articleNo);

  public Article getNextArticle(int articleNo, String boardCd, String search);

  public Article getPrevArticle(int articleNo, String boardCd, String search);

  public List<AttachFile> getAttachFileList(int articleNo);

  public void removeAttachFile(int attachFileNo);

  public Board getBoard(String boardCd);

  public void addComment(Comment comment);

  public void modifyComment(Comment comment);

  public void removeComment(int commentNo);

  public List<Comment> getCommentList(int articleNo);

  public AttachFile getAttachFile(int attachFileNo);

  public Comment getComment(int commentNo);
}

In the net.java_school.board package, create a BoardServiceImpl.java, which is an implementation of the BoardService interface.

BoardServiceImpl.java
package net.java_school.board;

import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import net.java_school.mapper.BoardMapper;

@Service
public class BoardServiceImpl implements BoardService {
  @Autowired
  private BoardMapper boardMapper;

  @Override
  public List<Article> getArticleList(String boardCd, String search, Integer startRecord, Integer endRecord) {
    HashMap<String, String> hashmap = new HashMap<String, String>();
    hashmap.put("boardCd", boardCd);
    hashmap.put("search", search);
    hashmap.put("start", startRecord.toString());
    hashmap.put("end", endRecord.toString());

    return boardMapper.selectListOfArticles(hashmap);
  }
    
  @Override 
  public int getTotalRecord(String boardCd, String search) {
    HashMap<String,String> hashmap = new HashMap<String,String>();
    hashmap.put("boardCd", boardCd);
    hashmap.put("search", search);
    
    return boardMapper.selectCountOfArticles(hashmap);
  }
    
  @Override
  public int addArticle(Article article) {
    return boardMapper.insert(article);
  }

  @Override
  public void addAttachFile(AttachFile attachFile) {
    boardMapper.insertAttachFile(attachFile);
  }

  @Override
  public void modifyArticle(Article article) {
    boardMapper.update(article);
  }

  @Override
  public void removeArticle(int articleNo) {
    boardMapper.delete(articleNo);
  }

  @Override
  public void increaseHit(int articleNo) {
    boardMapper.updateHitPlusOne(articleNo);
  }

  @Override
  public Article getArticle(int articleNo) {
    return boardMapper.selectOne(articleNo);
  }

  @Override
  public Article getNextArticle(int articleNo, String boardCd, String search) {
    HashMap<String, String> hashmap = new HashMap<String, String>();
    Integer no = articleNo;
    hashmap.put("articleNo", no.toString());
    hashmap.put("boardCd", boardCd);
    hashmap.put("search", search);

    return boardMapper.selectNextOne(hashmap);
  }

  @Override
  public Article getPrevArticle(int articleNo, String boardCd, String search) {
    HashMap<String, String> hashmap = new HashMap<String, String>();
    Integer no = articleNo;
    hashmap.put("articleNo", no.toString());
    hashmap.put("boardCd", boardCd);
    hashmap.put("search", search);

    return boardMapper.selectPrevOne(hashmap);
  }

  @Override
  public List<AttachFile> getAttachFileList(int articleNo) {
    return boardMapper.selectListOfAttachFiles(articleNo);
  }

  @Override
  public void removeAttachFile(int attachFileNo) {
    boardMapper.deleteFile(attachFileNo);
  }

  @Override
  public Board getBoard(String boardCd) {
    return boardMapper.selectOneBoard(boardCd);
  }

  @Override
  public void addComment(Comment comment) {
    boardMapper.insertComment(comment);
  }

  @Override
  public void modifyComment(Comment comment) {
    boardMapper.updateComment(comment);
  }

  @Override
  public void removeComment(int commentNo) {
    boardMapper.deleteComment(commentNo);
  }

  @Override
  public List<Comment> getCommentList(int articleNo) {
    return boardMapper.selectListOfComments(articleNo);
  }

  @Override
  public AttachFile getAttachFile(int attachFileNo) {
    return boardMapper.selectOneAttachFile(attachFileNo);
  }

  @Override
  public Comment getComment(int commentNo) {
    return boardMapper.selectOneComment(commentNo);
  }
}

Create a NumbersForPaging.java that saves numbers needed for paging.

NumbersForPaging.java
package net.java_school.commons;

public class NumbersForPaging {
  private int totalPage;
  private int firstPage;
  private int lastPage;
  private int prevBlock;
  private int nextBlock;
  private int listItemNo;
  
  public int getTotalPage() {
    return totalPage;
  }
  public void setTotalPage(int totalPage) {
    this.totalPage = totalPage;
  }
  public int getFirstPage() {
    return firstPage;
  }
  public void setFirstPage(int firstPage) {
    this.firstPage = firstPage;
  }
  public int getLastPage() {
    return lastPage;
  }
  public void setLastPage(int lastPage) {
    this.lastPage = lastPage;
  }
  public int getPrevBlock() {
    return prevBlock;
  }
  public void setPrevBlock(int prevBlock) {
    this.prevBlock = prevBlock;
  }
  public int getNextBlock() {
    return nextBlock;
  }
  public void setNextBlock(int nextBlock) {
    this.nextBlock = nextBlock;
  }
  public int getListItemNo() {
    return listItemNo;
  }
  public void setListItemNo(int listItemNo) {
    this.listItemNo = listItemNo;
  }
}

Create a Paginator.java that returns a NumberForPaging object.

Paginator.java
package net.java_school.commons;

public class Paginator {

  public NumbersForPaging getNumbersForPaging(int totalRecord, int page, int numPerPage, int pagePerBlock) {
    int totalPage = totalRecord / numPerPage;
    if (totalRecord % numPerPage != 0) totalPage++;
    int totalBlock = totalPage / pagePerBlock;
    if (totalPage % pagePerBlock != 0) totalBlock++;
    int block = page / pagePerBlock;
    if (page % pagePerBlock != 0) block++;
    int firstPage = (block - 1) * pagePerBlock + 1;
    int lastPage = block * pagePerBlock;
    int prevPage = 0;
    if (block > 1) {
      prevPage = firstPage - 1;
    }
    int nextPage = 0;
    if (block < totalBlock) {
      nextPage = lastPage + 1;
    }
    if (block >= totalBlock) {
      lastPage = totalPage;
    }
    int listItemNo = totalRecord - (page - 1) * numPerPage;
    
    NumbersForPaging numbers = new NumbersForPaging();
    
    numbers.setTotalPage(totalPage);
    numbers.setFirstPage(firstPage);
    numbers.setLastPage(lastPage);
    numbers.setPrevBlock(prevPage);
    numbers.setNextBlock(nextPage);
    numbers.setListItemNo(listItemNo);
    
    return numbers;
  }
}

Create a BbsController.java in the net.java_school.controller package.

BbsController.java
@Controller
@RequestMapping("/bbs")
public class BbsController extends Paginator {

  @Autowired
  private BoardService boardService;

  @GetMapping("/list")
  public String list(@RequestParam(name="boardCd") String boardCd, 
      @RequestParam(name="page") Integer page, 
      @RequestParam(name="search") String search,
      HttpServletRequest req,
      HttpSession session,
      Model model) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      return "redirect:/users/login?url=" + url;
    }

    int numPerPage = 10;
    int pagePerBlock = 10;

    int totalRecord = boardService.getTotalRecord(boardCd, search);

    NumbersForPaging numbers = this.getNumbersForPaging(totalRecord, page, numPerPage, pagePerBlock);
    //oracle
    Integer startRecord = (page - 1) * numPerPage + 1;
    Integer endRecord = page * numPerPage;

    HashMap<String, String> map = new HashMap<String, String>();
    map.put("boardCd", boardCd);
    map.put("search", search);
    map.put("start", startRecord.toString());
    map.put("end", endRecord.toString());
    List<Article> list = boardService.getArticleList(map);
    String boardName = boardService.getBoard(boardCd).getBoardNm_ko();
    
    Integer listItemNo = numbers.getListItemNo();
    Integer prevPage = numbers.getPrevBlock();
    Integer nextPage = numbers.getNextBlock();
    Integer firstPage = numbers.getFirstPage();
    Integer lastPage = numbers.getLastPage();

    model.addAttribute("list", list);
    model.addAttribute("boardName", boardName);
    model.addAttribute("listItemNo", listItemNo);
    model.addAttribute("prevPage", prevPage);
    model.addAttribute("nextPage", nextPage);
    model.addAttribute("firstPage", firstPage);
    model.addAttribute("lastPage", lastPage);

    return "bbs/list";
  }

  @GetMapping("/write")
  public String writeForm(@RequestParam(name="boardCd") String boardCd,
      HttpServletRequest req,
      HttpSession session,
      Model model) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      return "redirect:/users/login?url=" + url;
    }

    String boardName = boardService.getBoard(boardCd).getBoardNm_ko();
    model.addAttribute("boardName", boardName);

    return "bbs/write";
  }

  @PostMapping("write")
  public String write(Article article, 
      @RequestParam(name="attachFile") MultipartFile attachFile,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    if (user == null) {
      throw new AuthenticationException(WebContants.NOT_LOGIN);
    }

    article.setEmail(user.getEmail());
    boardService.addArticle(article);

    if (!attachFile.isEmpty()) {
      AttachFile file = new AttachFile();
      file.setFilename(attachFile.getOriginalFilename());
      file.setFiletype(attachFile.getContentType());
      file.setFilesize(attachFile.getSize());
      file.setArticleNo(article.getArticleNo());
      file.setEmail(user.getEmail());
      
      boardService.addAttachFile(file);
      
      File dir = new File(WebContants.UPLOAD_PATH + user.getEmail());
      if (!dir.exists()) dir.mkdirs();
      Path path = Paths.get(WebContants.UPLOAD_PATH + user.getEmail());
      try (InputStream inputStream = attachFile.getInputStream()) {
        Files.copy(inputStream, path.resolve(attachFile.getOriginalFilename()), StandardCopyOption.REPLACE_EXISTING);
      }
    }
    return "redirect:/bbs/list?page=1&boardCd=" + article.getBoardCd();
  }

  @GetMapping("/view")
  public String view(@RequestParam(name="articleNo") Integer articleNo, 
      @RequestParam(name="boardCd") String boardCd, 
      @RequestParam(name="page") Integer page,
      @RequestParam(name="search") String search,
      HttpServletRequest req,
      HttpSession session,
      Model model) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    if (user == null) {
      String url = req.getServletPath();
      String query = req.getQueryString();
      if (query != null) url += "?" + query;
      url = URLEncoder.encode(url, "UTF-8");
      return "redirect:/users/login?url=" + url;
    }

    boardService.increaseHit(articleNo);

    Article article = boardService.getArticle(articleNo);
    List<AttachFile> attachFileList = boardService.getAttachFileList(articleNo);
    Article nextArticle = boardService.getNextArticle(articleNo, boardCd, search);
    Article prevArticle = boardService.getPrevArticle(articleNo, boardCd, search);
    List<Comment> commentList = boardService.getCommentList(articleNo);

    String title = article.getTitle();
    String content = article.getContent();
    content = content.replaceAll(WebContants.LINE_SEPARATOR, "<br />");
    int hit = article.getHit();
    String name = article.getName();
    String email = article.getEmail();
    String regdate = article.getRegdateForView();

    model.addAttribute("title", title);
    model.addAttribute("content", content);
    model.addAttribute("hit", hit);
    model.addAttribute("name", name);
    model.addAttribute("email", email);
    model.addAttribute("regdate", regdate);
    model.addAttribute("attachFileList", attachFileList);
    model.addAttribute("nextArticle", nextArticle);
    model.addAttribute("prevArticle", prevArticle);
    model.addAttribute("commentList", commentList);

    int numPerPage = 10;
    int pagePerBlock = 10;

    int totalRecord = boardService.getTotalRecord(boardCd, search);

    NumbersForPaging numbers = this.getNumbersForPaging(totalRecord, page, numPerPage, pagePerBlock);

    Integer startRecord = (page - 1) * numPerPage + 1;
    Integer endRecord = page * numPerPage;

    HashMap<String, String> map = new HashMap<String, String>();
    map.put("boardCd", boardCd);
    map.put("search", search);
    map.put("start", startRecord.toString());
    map.put("end", endRecord.toString());
    List<Article> list = boardService.getArticleList(map);
    String boardName = boardService.getBoard(boardCd).getBoardNm_ko();
    
    int listItemNo = numbers.getListItemNo();
    int prevPage = numbers.getPrevBlock();
    int nextPage = numbers.getNextBlock();
    int firstPage = numbers.getFirstPage();
    int lastPage = numbers.getLastPage();

    model.addAttribute("list", list);
    model.addAttribute("listItemNo", listItemNo);
    model.addAttribute("prevPage", prevPage);
    model.addAttribute("firstPage", firstPage);
    model.addAttribute("lastPage", lastPage);
    model.addAttribute("nextPage", nextPage);
    model.addAttribute("boardName", boardName);

    return "bbs/view";
  }

  @PostMapping("/addComment")
  public String addComment(Integer articleNo, 
      String boardCd, 
      Integer page, 
      String search,
      String memo,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    if (user == null) {
      throw new AuthenticationException(WebContants.NOT_LOGIN);
    }

    Comment comment = new Comment();
    comment.setArticleNo(articleNo);
    comment.setEmail(user.getEmail());
    comment.setMemo(memo);

    boardService.addComment(comment);

    search = URLEncoder.encode(search,"UTF-8");

    return "redirect:/bbs/view?articleNo=" + articleNo + 
        "&boardCd=" + boardCd + 
        "&page=" + page + 
        "&search=" + search;
  }

  @PostMapping("/updateComment")
  public String updateComment(Integer commentNo, 
      Integer articleNo, 
      String boardCd, 
      Integer page, 
      String search, 
      String memo,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);

    Comment comment = boardService.getComment(commentNo);

    if (user == null || !user.getEmail().equals(comment.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    comment.setMemo(memo);
    boardService.modifyComment(comment);

    search = URLEncoder.encode(search, "UTF-8");

    return "redirect:/bbs/view?articleNo=" + articleNo + 
        "&boardCd=" + boardCd + 
        "&page=" + page + 
        "&search=" + search;
  }

  @PostMapping("/deleteComment")
  public String deleteComment(Integer commentNo, 
      Integer articleNo, 
      String boardCd, 
      Integer page, 
      String search,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);

    Comment comment = boardService.getComment(commentNo);

    if (user == null || !user.getEmail().equals(comment.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    boardService.removeComment(commentNo);

    search = URLEncoder.encode(search,"UTF-8");

    return "redirect:/bbs/view?articleNo=" + articleNo + 
        "&boardCd=" + boardCd + 
        "&page=" + page + 
        "&search=" + search;
  }

  @GetMapping("/modify")
  public String modifyForm(@RequestParam(name="articleNo") Integer articleNo, 
      @RequestParam(name="boardCd") String boardCd,
      HttpSession session, Model model) {

    User user = (User) session.getAttribute(WebContants.USER_KEY);

    Article article = boardService.getArticle(articleNo);

    if (user == null || !user.getEmail().equals(article.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    String title = article.getTitle();
    String content = article.getContent();
    String boardName = boardService.getBoard(boardCd).getBoardNm_ko();

    model.addAttribute("title", title);
    model.addAttribute("content", content);
    model.addAttribute("boardName", boardName);

    return "bbs/modify";
  }

  @PostMapping("/modify")
  public String modify(Article article, 
      @RequestParam(name="attachFile") attachFile,
      @RequestParam(name="page") Integer page,
      @RequestParam(name="search") String search,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);

    int articleNo = article.getArticleNo();
    String ownerEmail = boardService.getArticle(articleNo).getEmail();
    if (!ownerEmail.equals(user.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    boardService.modifyArticle(article);
    
    if (!attachFile.isEmpty()) {
      AttachFile file = new AttachFile();
      file.setFilename(attachFile.getOriginalFilename());
      file.setFiletype(attachFile.getContentType());
      file.setFilesize(attachFile.getSize());
      file.setArticleNo(article.getArticleNo());
      file.setEmail(user.getEmail());
      
      boardService.addAttachFile(file);
      
      File dir = new File(WebContants.UPLOAD_PATH + user.getEmail());
      if (!dir.exists()) dir.mkdirs();
      Path path = Paths.get(WebContants.UPLOAD_PATH + user.getEmail());
      try (InputStream inputStream = attachFile.getInputStream()) {
        Files.copy(inputStream, path.resolve(attachFile.getOriginalFilename()), StandardCopyOption.REPLACE_EXISTING);
      }
    }

    search = URLEncoder.encode(search, "UTF-8");
    return "redirect:/bbs/view?articleNo=" + articleNo 
        + "&boardCd=" + boardCd 
        + "&page=" + page 
        + "&search=" + search;
  }

  @PostMapping("/deleteAttachFile")
  public String deleteAttachFile(Integer attachFileNo, 
      Integer articleNo, 
      String boardCd, 
      Integer page, 
      String search,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    AttachFile attachFile = boardService.getAttachFile(attachFileNo);

    if (user == null || !user.getEmail().equals(attachFile.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    boardService.removeAttachFile(attachFileNo);

    search = URLEncoder.encode(search, "UTF-8");

    return "redirect:/bbs/view?articleNo=" + articleNo + 
        "&boardCd=" + boardCd + 
        "&page=" + page + 
        "&search=" + search;
  }

  @PostMapping("/del")
  public String del(Integer articleNo, 
      String boardCd, 
      Integer page, 
      String search,
      HttpSession session) throws Exception {

    User user = (User) session.getAttribute(WebContants.USER_KEY);
    Article article = boardService.getArticle(articleNo);

    if (user == null || !user.getEmail().equals(article.getEmail())) {
      throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
    }

    boardService.removeArticle(articleNo);

    search = URLEncoder.encode(search, "UTF-8");

    return "redirect:/bbs/list?boardCd=" + boardCd + 
        "&page=" + page + 
        "&search=" + search;
  }
}

A handler method parameters like boardCd, page, and search are assigned values of the same name's request parameters. If the name of the method parameter and the request parameter is different, put @RequestParam annotation before the method parameter name below to solve it.

If the method parameter name is page and the request parameter name is page, you should declare the handler method parameter, page like below.

@RequestParam(name="page") Integer page

The following writeForm() method is the handler for the GET method's /bbs/write request.

@GetMapping("write")
public String writeForm(@RequestParam(name="boardCd") String boardCd, 
		HttpServletRequest req, HttpSession session...)

The following write() method handles the POST method's /bbs/write request.

@PostMapping("write")
public String write(Article article, 
		HttpSession session, 
		@RequestParam(name="attachFile") MultipartFile attachFile) throws Exception {

The write() method first checks whether the user logs in. If the user did not log in, an AuthenticationException exception occurs, and the view resolver selects /WEB-INF/views/403-error.jsp as the view.

//login check
User user = (User) session.getAttribute(WebContants.USER_KEY);
if (user == null) {
  throw new AuthenticationException(WebContants.NOT_LOGIN);
}

The code checks whether the user who submits a new post is a logged-in user and inserts the post into the table if they are logged-in users. When boardService.addArticle(article) is executed, the article's setArticleNo() is called to set the post's unique number.

article.setBoardCd(article.getBoardCd());
article.setTitle(article.getTitle());
article.setContent(article.getContent());
article.setEmail(user.getEmail());
boardService.addArticle(article);

If there is a attachment, insert the file information into the attachfile table.

if (!attachFile.isEmpty()) {
	AttachFile file = new AttachFile();
	file.setFilename(attachFile.getOriginalFilename());
	file.setFiletype(attachFile.getContentType());
	file.setFilesize(attachFile.getSize());
	file.setArticleNo(article.getArticleNo());
	file.setEmail(user.getEmail());
	boardService.addAttachFile(file);

The method moves the attachment to the upload directory.

if (!attachFile.isEmpty()) {
	
	//..omit..
	
	File dir = new File(WebContants.UPLOAD_PATH + user.getEmail());
	if (!dir.exists()) dir.mkdirs();
	Path path = Paths.get(WebContants.UPLOAD_PATH + user.getEmail());
	
	try (InputStream inputStream = attachFile.getInputStream()) {
		Files.copy(InputStream, path.resolve(attachFile.getOriginalFilename()), StandardCopyOption.REPLACE_EXISTING);
	}
}

The code above uses two new methods, addArticle(article) and addAttachFile(attachFile).
Note that the board source of the JSP Project chapter used only one method, addArticle(article, attachFile), in the same process.

After completing all the work related to registering a new post, the method redirects the request to the bulletin board list. Note that there are page=1 and boardCd parameters in the query string of the redirect address.

return redirect:/bbs/list?page=1&boardCd=" + article.getBoardCd();

Detailed view

The following method is a handler for /bbs/view requests of the GET method.

@GetMapping("/view")
public String view(@RequestParam(name="articleNo") Integer articleNo,
        @RequestParam(name="boardCd") String boardCd,
        @RequestParam(name="page") Integer page,
        @RequestParam(name="search") String search,
        HttpServletRequest req, HttpSession session, Model model) throws Exception {

The method first checks whether the user is a logged-in user and, if a logged-in user, increases the number of views and builds the data required for the detailed view screen.

boardService.increaseHit(articleNo);

Article article = boardService.getArticle(articleNo);
List<AttachFile> attachFileList = boardService.getAttachFileList(articleNo);
Article nextArticle = boardService.getNextArticle(articleNo, boardCd, search);
Article prevArticle = boardService.getPrevArticle(articleNo, boardCd, search);
List<Comment> commentList = boardService.getCommentList(articleNo);
String boardName = boardService.getBoard(boardCd).getBoardNm_ko();

String title = article.getTitle();
String content = article.getContent();
content = content.replaceAll(WebContants.LINE_SEPARATOR, "<br />");
int hit = article.getHit();
String name = article.getName();
String email = article.getEmail();
String regdate = article.getRegdateForView();

model.addAttribute("title", title);
model.addAttribute("content", content);
model.addAttribute("hit", hit);
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("regdate", regdate);
model.addAttribute("attachFileList", attachFileList);
model.addAttribute("nextArticle", nextArticle);
model.addAttribute("prevArticle", prevArticle);
model.addAttribute("commentList", commentList);

Add comment

The addComment() is a handler for /bbs/addComment requests of POST method.

@PostMapping("/addComment")
public String addComment(@RequestParam(name="articleNo") Integer articleNo, 
        @RequestParam(name="boardCd") String boardCd, 
        @RequestParam(name="page") Integer page, 
        @RequestParam(name="search") String search,
        @RequestParam(name="memo") String memo,
        HttpSession session) throws Exception {

That method first checks if the user logs in.

User user = (User) session.getAttribute(WebContants.USER_KEY);
if (user == null) {
  throw new AuthenticationException(WebContants.NOT_LOGIN);
}

The method inserts the comment information to the table using request parameters if the user has logged in.

Comment comment = new Comment();
comment.setArticleNo(articleNo);
comment.setEmail(user.getEmail());
comment.setMemo(memo);

boardService.addComment(comment);

The search term needs to be encoded.

search = URLEncoder.encode(search, "UTF-8");

The method redirects the request to view.jsp.

return "redirect:/bbs/view?articleNo=" + articleNo + 
    "&boardCd=" + boardCd + 
    "&page=" + page + 
    "&search=" + search;

The updateComment() method is a handler for "/bbs/updateComment" request of POST method.

@PostMapping("/updateComment")
public String updateComment(
        @RequestParam(name="commentNo") Integer commentNo, 
        @RequestParam(name="articleNo") Integer articleNo, 
        @RequestParam(name="boardCd") String boardCd, 
        @RequestParam(name="page") Integer page, 
        @RequestParam(name="search") String search, 
        @RequestParam(name="memo") String memo,
        HttpSession session) throws Exception {

The method first checks whether the user has logged in and is the text owner and throws a custom authentication exception if the user is not a logged-in user or is not the owner of the post.

User user = (User) session.getAttribute(WebContants.USER_KEY);

Comment comment = boardService.getComment(commentNo);

if (user == null || !user.getEmail().equals(comment.getEmail())) {
  throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

If the above code passes, the method modifies the comment with the request parameters. We can edit only the comment's content, and the original owner will not change, even if the administrator has edited the text.

comment.setMemo(memo);
boardService.modifyComment(comment);

The method redirects the request to view.jsp.

search = URLEncoder.encode(search, "UTF-8");

return "redirect:/bbs/view?articleNo=" + articleNo + 
    "&boardCd=" + boardCd + 
    "&page=" + page + 
    "&search=" + search;

The deleteComment() is a handler for /bbs/deleteComment requests of POST method.

@PostMapping("/deleteComment")
public String deleteComment(
        @RequestParam(name="commentNo") Integer commentNo, 
        @RequestParam(name="articleNo") Integer articleNo, 
        @RequestParam(name="boardCd") String boardCd, 
        @RequestParam(name="page") Integer page, 
        @RequestParam(name="search") String search,
        HttpSession session) throws Exception {

This method first checks whether the user has logged in and is the text owner and throws a custom authentication exception if the user is not a logged-in user or is not the owner of the comment.

User user = (User) session.getAttribute(WebContants.USER_KEY);

Comment comment = boardService.getComment(commentNo);

if (user == null || !user.getEmail().equals(comment.getEmail())) {
  throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

If the above passes, the method deletes the comment with the request parameter.

boardService.removeComment(commentNo);

The method redirects the request to view.jsp.

search = URLEncoder.encode(search, "UTF-8");

return "redirect:/bbs/view?articleNo=" + articleNo + 
    "&boardCd=" + boardCd + 
    "&page=" + page + 
    "&search=" + search;

Attachment Download

create a dedicated attachment download controller to handle all requests starting with /data.

DownloadController.java
package net.java_school.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.nio.file.Paths;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import net.java_school.commons.WebContants;
import net.java_school.board.BoardService;
import net.java_school.board.AttachFile;
import net.java_school.exception.AuthenticationException;
import net.java_school.user.User;
import org.springframework.beans.factory.annotation.Autowired;

@Controller
public class DownloadController {
	
	@Autowired
	private BoardService boardService;

	@GetMapping("data")
	public ResponseEntity<InputStreamResource> download(
			@RequestParam(name="fileno") Integer fileno, 
			HttpServletRequest req, HttpSession session) throws IOException {
		
		//Login check
		User user = (User) session.getAttribute(WebContants.USER_KEY);
		if (user == null) {
		    throw new AuthenticationException(WebContants.NOT_LOGIN);
		}
					
		AttachFile attachFile = boardService.getAttachFile(fileno);
		String owner = attachFile.getEmail();
		String filename = attachFile.getFilename();

		//prevent path traversal code start
		Path ownerPath = Paths.get(WebContants.UPLOAD_PATH + owner).toAbsolutePath().normalize();
		Path filePath = ownerPath.resolve(filename);
		Path normalizedFilePath = filePath.normalize();
		if (!normalizedFilePath.startsWith(ownerPath)) {
			throw new SecurityException("Path Traversal (" + filename + ")");
		}
		File file = normalizedFilePath.toFile();
		if (!file.exists() || !file.isFile()) {
			throw new FileNotFoundException("The file does not exist!");
		}
		//prevent path traversal code end
		
		InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

		boolean ie = req.getHeader("User-Agent").indexOf("MSIE") != -1;
		if (ie) {
			filename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", " ");
		} else {
			filename = new String(filename.getBytes("UTF-8"), "8859_1");
		}

		return ResponseEntity.ok()
				.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + filename + "\"")
				.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(file.length())
				.body(resource);
	}
}

The download controller should set the Content-Disposition response header to attachment to prompt the browser to display a file save dialog. Setting the Content-Type is also crucial; using the generic binary type application/octet-stream is the safest way to force the browser to download the file. Providing the Content-Length allows the download dialog to display the progress percentage and estimated time remaining for large files.

Add the following to #form-group.

<c:url var="dataUrl" value="/data" />
<form id="download" action="${dataUrl}" method="get">
	<input type="hidden" name="fileno"/>
</form>

Modify the download link in view.jsp as shown below.

<a href="javascript:download('${file.attachFileNo }')">${file.filename }</a>

Post Editing Form

modifyForm() is a method mapped to the GET /bbs/modify request.

@GetMapping("/modify")
public String modifyForm(
        @RequestParam(name="articleNo") Integer articleNo, 
        @RequestParam(name="boardCd") String boardCd,
        HttpSession session, Model model) {

Checks whether the logged-in user is the owner of the post.

User user = (User) session.getAttribute(WebContants.USER_KEY);

Article article = boardSe
rvice.getArticle(articleNo);

if (user == null || !user.getEmail().equals(article.getEmail())) {
    throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

Passes the information required for the post editing form to the view.

String boardName = boardService.getBoard(boardCd).getBoardNm_ko();
        
model.addAttribute("boardName", boardName);
model.addAttribute("article", article);

return "bbs/modify";

Edit post

modify() is a method mapped to the POST /bbs/modify request.

@PostMapping("/modify")
public String modify(Article article,
		@RequestParam(name="page") Integer page,
		@RequestParam(name="search") String search,
		HttpSession session,
		@RequestParam(name="attachFile") MultipartFile attachFile) throws Exception {

Checks whether the logged-in user is the owner of the post.

User user = (User) session.getAttribute(WebContants.USER_KEY);

Article modifyTargetArticle = boardService.getArticle(article.getArticleNo());

if (!modifyTargetArticle.getEmail().equals(user.getEmail())) {
  throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}
article.setEmail(user.getEmail());
boardService.modifyArticle(article);

If there are attached files, insert the file information into the table.

if (!attachFile.isEmpty()) {
	AttachFile file = new AttachFile();
	file.setFilename(file.getOriginalFilename());
	file.setFiletype(file.getContentType());
	file.setFilesize(file.getSize());
	file.setArticleNo(article.getArticleNo());
	boardService.addAttachFile(file);
	
	//TODO 
}

Move the file to the desired location.

if (!attachFile.isEmpty()) {

	//..Omit..
		
	File dir = new File(WebContants.UPLOAD_PATH + user.getEmail());
	if (!dir.exists()) dir.mkdirs();
	Path path = Paths.get(WebContants.UPLOAD_PATH + user.getEmail());
	try (InputStream inputStream = attachFile.getInputStream()) {
		Files.copy(
				inputStream, 
				path.resolve(attachFile.getOriginalFilename()), 
				StandardCopyOption.REPLACE_EXISTING
		);
	}
}

Finally, redirect to the detail view.

search = URLEncoder.encode(search,"UTF-8");

return "redirect:/bbs/view?articleNo=" + article.getArticleNo() 
    + "&boardCd=" + article.getBoardCd() 
    + "&page=" + page 
    + "&search=" + search;

Delete Attachment Info

deleteAttachFile() is a method mapped to the POST /bbs/deleteAttachFile request.

@PostMapping("/deleteAttachFile")
public String deleteAttachFile(
        Integer attachFileNo, 
        Integer articleNo, 
        String boardCd, 
        Integer page, 
        String search,
        HttpSession session) throws Exception {

Checks whether the logged-in user is the owner of the attachment.

User user = (User) session.getAttribute(WebContants.USER_KEY);
AttachFile attachFile = boardService.getAttachFile(attachFileNo);

if (user == null || !user.getEmail().equals(attachFile.getEmail())) {
    throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

After deleting the attachment information, redirect to the detail view.

boardService.removeAttachFile(attachFileNo);

search = URLEncoder.encode(search, "UTF-8");

return "redirect:/bbs/view?articleNo=" + articleNo + 
    "&boardCd=" + boardCd + 
    "&page=" + page + 
    "&search=" + search;

Delete post

del() is a method mapped to the POST /bbs/del request.

@PostMapping("/del")
public String del(
        Integer articleNo, 
        String boardCd, 
        Integer page, 
        String search,
        HttpSession session) throws Exception {

Checks whether the logged-in user is the owner of the post.

User user = (User) session.getAttribute(WebContants.USER_KEY);
Article article = boardService.getArticle(articleNo);

if (user == null || !user.getEmail().equals(article.getEmail())) {
    throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

After deleting the post information, redirect to the list.

boardService.removeArticle(articleNo);

search = URLEncoder.encode(search, "UTF-8");

return "redirect:/bbs/list?boardCd=" + boardCd + 
    "&page=" + page + 
    "&search=" + search;

Test

 
mvn clean compile war:inplace

Modify ROOT.xml as shown below.

ROOT.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context
  docBase="C:/www/jwpsamples/src/main/webapp"
  reloadable="true">
</Context>

Restart Tomcat and visit the following:
http://localhost:8080/bbs/list?boarCd=smalltalk&page=1.

Authentication Summary

The following is the authentication code used in the method of the bulletin board controller.

User user = (User) session.getAttribute(WebContants.USER_KEY);
if (user == null) {
  String url = req.getServletPath();
  String query = req.getQueryString();
  if (query != null) url += "?" + query;
  url = URLEncoder.encode(url, "UTF-8");
  return "redirect:/users/login?url=" + url;
}
User user = (User) session.getAttribute(WebContants.USER_KEY);
if (user == null) {
  throw new AuthenticationException(WebContants.NOT_LOGIN);
}
if (user == null || !user.getEmail().equals(comment.getEmail())) {
  throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}

The following is the authentication code used in the method of the UsersController.

User user = userService.login(email, passwd);
if (user == null) {
  return "redirect:/users/login?url=" + url + "&msg=Login-Failed";
} else {
  session.setAttribute(WebContants.USER_KEY, user);
  if (url != null && !url.equals("")) {
  return "redirect:" + url;
}

return "redirect:/";
User user = (User) session.getAttribute(WebContants.USER_KEY);
if (user == null) {
  String url = req.getServletPath();
  String query = req.getQueryString();
  if (query != null) url += "?" + query;
  url = URLEncoder.encode(url, "UTF-8");
  return "redirect/users/login?url=" + url;
}
User loginUser = (User) session.getAttribute(WebContants.USER_KEY);

if (loginUser == null) {
  throw new AuthenticationException(WebContants.NOT_LOGIN);
}

if (userService.login(loginUser.getEmail(), user.getPasswd()) == null) {
  throw new AuthenticationException(WebContants.AUTHENTICATION_FAILED);
}
String email = ((User)session.getAttribute(WebContants.USER_KEY)).getEmail();

The following is the authentication code used in JSPs.

<!-- omit -->

<p id="file-list" style="text-align: right">
  <c:forEach var="file" items="${attachFileList }" varStatus="status">    
  <a href="javascript:download('${file.filename }')">${file.filename }</a>
  <c:if test="${user.email == file.email }">
  <a href="javascript:deleteAttachFile('${file.attachFileNo }')">x</a>
  </c:if>
  <br />
  </c:forEach>
</p>

<!-- omit -->

<c:if test="${user.email == comment.email }">    
<span class="modify-del">
  <a href="javascript:modifyCommentToggle('${comment.commentNo }')">Modify</a>
  | <a href="javascript:deleteComment('${comment.commentNo }')">Del</a>
</span>
</c:if>  

<!-- omit -->

<c:if test="${user.email == email }">
<div class="fl">
  <input type="button" value="Modify" onclick="goModify()" />
  <input type="button" value="Del" onclick="goDelete()"/>
</div>
</c:if>
References