Board program on Spring MVC

This article covers modifying the Model 2 project to Spring MVC project.
You need the final source of 'Building Spring MVC with maven' and the final source of the Model 2 bulletin board of the JSP Porject chapter.
If Git be installed on your system, you can download the Model 2 final source with the following command:

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

Database design

See Database design.

Stylesheets and Images

Copy the /css and /images folders from C:/www/model2/WebContent on the Model 2 bulletin board and paste them into the Document Base, C:/www/spring-bbs/src/main/webapp directory.

JSPs

Create the /views folder under C:/www/spring-bbs/src/main/webapp/WEB-INF.
Copy the index.jsp, error.jsp, /bbs, /inc, /java, /users folders from the Document Base of Model 2 board and paste them into the /views folder.

The reason for putting the JSP in the /WEB-INF subdirectory is to prevent the web browser from directly accessing JSPs.
Spring MVC recommends that all requests be passed to the controller through the dispatcher servlet.

Java sources

Copy all the Java source folders from C:/www/model2/src/net/java_school on the Model 2 bulletin board and paste them into C:/www/spring-bbs/src/main/java.

Modify JSP

As recommended by Spring MVC, set the following in web.xml so that the dispatcher servlet will handle all requests:

<servlet-mapping>
	<servlet-name>spring-bbs</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>

Change the request string within JSPs

The Model 2 board controller was only responsible for requests ending in .do.
Therefore, you must modify all request strings ending in .do in the JSP of the Model 2 bulletin board.
Remove .do from request strings ending in .do in any JSP.
For example, modify ../users/login.do in header.jsp to ../users/login.

Change date format in list.jsp and view.jsp

The date formats in list.jsp and view.jsp will be different.
Add the following tag libraries to both files:

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

In list.jsp, find the part that displays the date and modify it as follows.

<fmt:formatDate pattern="yyyy.MM.dd" value="${article.regdate }" />

In view.jsp, find the part that displays the date and modify it as follows.

<fmt:formatDate pattern="yyyy.MM.dd HH:mm:ss" value="${regdate }" />
<fmt:formatDate pattern="yyyy.MM.dd" value="${article.regdate }" />

Create download.jsp

The view.jsp on the Model 2 bulletin board simply links the attachment.
On the Spring MVC bulletin board, the download.jsp be used to download the attachment.
With download.jsp, you can download attachments even if they are in a location that your web browser can not access.

/WEB-INF/views/inc/download.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>	  
<%@ page import="java.io.File" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="java.io.OutputStream" %>
<%@ page import="java.io.FileInputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.springframework.util.FileCopyUtils" %>
<%@ page import="net.java_school.commons.WebContants" %>
<%
//request.setCharacterEncoding("UTF-8");//This is done by a filter.
String filename = request.getParameter("filename");

File file = new File(WebContants.UPLOAD_PATH + filename);

String filetype = filename.substring(filename.indexOf(".") + 1, filename.length());

if (filetype.trim().equalsIgnoreCase("txt")) {
  response.setContentType("text/plain");
} else {
  response.setContentType("application/octet-stream");
}

response.setContentLength((int) file.length());

boolean ie = request.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");
}

response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");

OutputStream outputStream = response.getOutputStream();
FileInputStream fis = null;

try {
  fis = new FileInputStream(file);
  FileCopyUtils.copy(fis, outputStream);
} finally {
  if (fis != null) {
    try {
      fis.close();
    } catch (IOException e) {}
  }
}

out.flush();
%>

In the above, WebContants.UPLOAD_PATH is the directory where the attachment is located. Add UPLOAD_PATH to WebContants.java.

WebContants.java
package net.java_school.commons;

public class WebContants {
  //Session key
  public final static String USER_KEY = "user";
  //Error Message
  public final static String NOT_LOGIN = "Not Login";
  public final static String AUTHENTICATION_FAILED = "Authentication Failed";
  //Line Separator
  public final static String LINE_SEPARATOR = System.getProperty("line.separator");
  //Upload Path
  public final static String UPLOAD_PATH = "C:/www/spring-bbs/upload/";
}

C:/www/spring-bbs/upload/ is a directory that the web browser can not access.
Therefore, you can not download attachments as links.
Note that the attachment download code in view.jsp must be changed.

Logging

Create the following file in C:/www/spring-bbs/src/main/resources.
If the /resources folder 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>
	<Appenders>
		<File name="A1" fileName="A1.log" append="true">
			<PatternLayout pattern="%t %-5p %c{2} - %m%n" />
		</File>
		<Console name="STDOUT" target="SYSTEM_OUT">
			<PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
		</Console>
	</Appenders>
	<Loggers>
		<Logger name="net.java_school" level="debug">
			<AppenderRef ref="A1" />
		</Logger>
		<Root level="debug">
			<AppenderRef ref="STDOUT" />
		</Root>
	</Loggers>
</Configuration>

Membership

Database-related code will be modified to use the MyBatis-Spring.

Change UserService.java to 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.mybatis package.

UserMapper.java
package net.java_school.mybatis;

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.mybatis.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);
  }
    
}

In the project explorer view, create the net.java_school.mybatis package in src/main/resources, and create the MyBatis related configuration file Congifuration.xml in this package.
This file sets the alias of the type and the location of the mapper files.
The configuration file to be created in the classpath in the Maven project must be created in src/main/resources.
If you create a configuration file in the source directory in the Maven project, it will not be copied to the classpath.

Configuration.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> 

  <settings>
    <setting name="logImpl" value="LOG4J2"/>
  </settings>
  	    
  <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>

  <mappers>
    <mapper resource="net/java_school/mybatis/BoardMapper.xml" />
    <mapper resource="net/java_school/mybatis/UserMapper.xml" />
  </mappers>

</configuration>

Create a UserMapper.xml file in the same location as Configuration.xml.
In the mapper file, the id must match the method name 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.mybatis.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 UsersController.java in the net.java_school.controller package.
If you want this controller to handle all requests involving "/users", add the @Controller annotation to the class declaration and add the @RequestMapping ("users") annotation right below the @Controller annotation.

UsersController.java
package net.java_school.controller;

import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;
import javax.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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

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

  @RequestMapping(value="signUp", method=RequestMethod.GET)
  public String signUp() {
    return "users/signUp";
  }

  @RequestMapping(value="signUp", method=RequestMethod.POST)
  public String signUp(User user) {
    userService.addUser(user);
    return "redirect:/users/welcome";
  }

  @RequestMapping(value="welcome", method=RequestMethod.GET)
  public String welcome() {
    return "users/welcome";
  }

  @RequestMapping(value="login", method=RequestMethod.GET)
  public String login() {
    return "users/login";
  }
    
  @RequestMapping(value="login", method=RequestMethod.POST)
  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:/";
    }
        
  }
        
  @RequestMapping(value="editAccount", method=RequestMethod.GET)
  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";
  }
    
  @RequestMapping(value="editAccount", method=RequestMethod.POST)
  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";
        
  }
    
  @RequestMapping(value="changePasswd", method=RequestMethod.GET)
  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";
  }
    
  @RequestMapping(value="changePasswd", method=RequestMethod.POST)
  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";
        
  }
    
  @RequestMapping(value="changePasswd_confirm", method=RequestMethod.GET)
  public String changePasswd_confirm() {
    return "users/changePasswd_confirm";
  }
    
  @RequestMapping(value="bye", method=RequestMethod.GET)
  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";
  }

  @RequestMapping(value="bye", method=RequestMethod.POST)
  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";
        
  }
  
  @RequestMapping(value="bye_confirm", method=RequestMethod.GET)
  public String bye_confirm() {
  
    return "users/bye_confirm";	  
  } 
    
  @RequestMapping(value="logout", method=RequestMethod.GET)
  public String logout(HttpSession session) {
    session.removeAttribute(WebContants.USER_KEY);

    return "redirect:/";

  }

}

The @RequestMapping(value="signUp", method=RequestMethod.GET) 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)

@RequestMapping(value="/signUp", method=RequestMethod.POST) 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 get your web browser redirects the request to welcome.jsp after completing membership.
If this method changes the screen using forwarding after membership, the user can try to 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.

@RequestMapping(value="welcome", method="RequestMethod.GET")

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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/")
public class HomeController {
    @RequestMapping(method=RequestMethod.GET)
    public String index() {
        return "index";
    }
}

The JavaController is the controller that handles all requests that contain "/java".

JavaController.java
package net.java_school.controller;

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

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

    @RequestMapping(method=RequestMethod.GET)
    public String index() {
        return "java/index";
    }
    
    @RequestMapping(value="jdk-install", method=RequestMethod.GET)
    public String basic() {
        return "java/jdk-install";
    }

}

The JavascriptController is the controller that handles all requests that contain "/javascript".

JavascriptController.java
package net.java_school.controller;

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

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

    @RequestMapping(method=RequestMethod.GET)
    public String index() {
        return "javascript/index";
    }
    
}

Since spring-bbs is the name of DispatcherServlet in web.xml, create sprng-bbs-servlet.xml file in /WEB-INF folder.

spring-bbs-servlet.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 location="/images/" mapping="/images/**" />
  <mvc:resources location="/css/" mapping="/css/**" />
        
  <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.mybatis" />

  <bean id="dataSource" class="org.apache.commons.dbcp.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="maxActive" value="100"/>
    <property name="maxWait" 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:net/java_school/mybatis/Configuration.xml" />
  </bean>

  <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="2097152" />
  </bean>
    
  <!-- ViewResolver -->
  <bean id="internalResourceViewResolver" 
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass">
      <value>org.springframework.web.servlet.view.JstlView</value>
    </property>
    <property name="prefix">
      <value>/WEB-INF/views/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </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>

spring-bbs-servlet.xml

The following tells Dispatcher servlet the location of the static resource.

<mvc:resources location=".." />

The next setting will automatically scan the bean and register it in the container.
The scanned bean does not need to be registered in the Spring configuration file.
In order for a bean to auto-scan, it must belong to the base-package package and have an annotation indicating that it is a component above the class declaration.
Classes with @Controller and @Repository annotations above the class declaration are treated as annotation names.
@Repository is used for DAO class.
The @Component and @Service annotations above the class declaration only indicate that the class is only auto-scanned, and nothing else is special.

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

Spring applications that run on an annotation base require the following settings:

<mvc:annotation-driven />

The following settings scan mybatis-spring beans and register them in the container.

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

Using the above configuration, you can omit the following settings from the Spring configuration file.

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

The dataSource definition is always needed whether you use Spring JDBC or MyBatis.

<bean id="dataSource" ..>

All MyBatis applications must use instances of the SqlSessionFactory interface type.
MyBatis Spring uses the SqlSessionFactoryBean.

<bean id="sqlSessionFactory" ..>

The following are settings for uploading files.
When uploading a file, a wrapper for the HttpServletRequest is passed to the controller.
The method that handles the uploaded file must cast the wrapper to the MultiPartHttpServletRequest interface to handle the multipart file passed to the server.

<bean id="multipartResolver" ..>

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">

The following are the settings for Spring MVC exception handling.
Spring MVC has several exception handling methods.
Among them, it is easiest to use SimpleMappingExceptionResolver.

If an AuthenticationException exception is thrown, the 403-error view is set to be selected.
The 403-error is interpreted as /WEB-INF/views/403-error.jsp by the view resolver.

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

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

If any other exception occurs, the following setting handles the error.

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

Board

Create the BoardMapper.xml file in the same location as the UserMapper.xml, as configured in Configuration.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.mybatis.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="searchWord != null and searchWord != ''">
                    AND (title LIKE '%${searchWord}%' OR DBMS_LOB.INSTR(content, #{searchWord}) > 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="searchWord != null and searchWord != ''">
            AND (title LIKE '%${searchWord}%' OR DBMS_LOB.INSTR(content, #{searchWord}) > 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="searchWord != null and searchWord != ''">
                    AND (title LIKE '%${searchWord}%' OR DBMS_LOB.INSTR(content, #{searchWord}) > 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="searchWord != null and searchWord != ''">
                    AND (title LIKE '%${searchWord}%' OR DBMS_LOB.INSTR(content, #{searchWord}) > 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>

How to make MyBatis return a unique number after an insert

Add useGeneratedKeys="true".
For databases that do not have auto-increment columns lide Oracle, add the selectKey subelement as follows:

<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 as shown below.

BoardMapper.java
package net.java_school.mybatis;

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 searchWord, Integer startRecord, Integer endRecord);

  public int getTotalRecord(String boardCd, String searchWord);

  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 searchWord);

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

  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 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.mybatis.BoardMapper;

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

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

    return boardMapper.selectListOfArticles(hashmap);
  }
    
  @Override 
  public int getTotalRecord(String boardCd, String searchWord) {
    HashMap<String,String> hashmap = new HashMap<String,String>();
    hashmap.put("boardCd", boardCd);
    hashmap.put("searchWord", searchWord);
    
    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 searchWord) {
    HashMap<String, String> hashmap = new HashMap<String, String>();
    Integer no = articleNo;
    hashmap.put("articleNo", no.toString());
    hashmap.put("boardCd", boardCd);
    hashmap.put("searchWord", searchWord);

    return boardMapper.selectNextOne(hashmap);
  }

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

    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 NumbersForPaging.java that save numbers needed for paging as shown below.

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 Paginator.java that returns a NumberForPaging object as shown below.

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 the BbsController.java class in the net.java_school.controller package.

BbsController.java
package net.java_school.controller;

import java.io.File;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import net.java_school.board.Article;
import net.java_school.board.AttachFile;
import net.java_school.board.BoardService;
import net.java_school.board.Comment;
import net.java_school.commons.WebContants;
import net.java_school.exception.AuthenticationException;
import net.java_school.user.User;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

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

	@Autowired
	private BoardService boardService;

	@RequestMapping(value="/list", method=RequestMethod.GET)
	public String list(String boardCd, 
			Integer page, 
			String searchWord,
			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, searchWord);

		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("searchWord", searchWord);
		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";

	}

	@RequestMapping(value="/write", method=RequestMethod.GET)
	public String writeForm(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";
	}

	@RequestMapping(value="/write", method=RequestMethod.POST)
	public String write(MultipartHttpServletRequest mpRequest,
			HttpSession session) throws Exception {

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

		String boardCd = mpRequest.getParameter("boardCd");
		String title = mpRequest.getParameter("title");
		String content = mpRequest.getParameter("content");

		Article article = new Article();
		article.setBoardCd(boardCd);
		article.setTitle(title);
		article.setContent(content);
		article.setEmail(user.getEmail());

		boardService.addArticle(article);

		Iterator<String> it = mpRequest.getFileNames();
		List<MultipartFile> fileList = new ArrayList<MultipartFile>();
		while (it.hasNext()) {
			MultipartFile multiFile = mpRequest.getFile((String) it.next());
			if (multiFile.getSize() > 0) {
				String filename = multiFile.getOriginalFilename();
				multiFile.transferTo(new File(WebContants.UPLOAD_PATH + filename));
				fileList.add(multiFile);
			}
		}

		int size = fileList.size();
		for (int i = 0; i < size; i++) {
			MultipartFile mpFile = fileList.get(i);
			AttachFile attachFile = new AttachFile();
			String filename = mpFile.getOriginalFilename();
			attachFile.setFilename(filename);
			attachFile.setFiletype(mpFile.getContentType());
			attachFile.setFilesize(mpFile.getSize());
			attachFile.setArticleNo(article.getArticleNo());
			attachFile.setEmail(user.getEmail());
			boardService.addAttachFile(attachFile);
		}

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

	@RequestMapping(value="/view", method=RequestMethod.GET)
	public String view(Integer articleNo, 
			String boardCd, 
			Integer page,
			String searchWord,
			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, searchWord);
		Article prevArticle = boardService.getPrevArticle(articleNo, boardCd, searchWord);
		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, searchWord);

		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("searchWord", searchWord);
		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";
	}

	@RequestMapping(value="/addComment", method=RequestMethod.POST)
	public String addComment(Integer articleNo, 
			String boardCd, 
			Integer page, 
			String searchWord,
			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);

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

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

	}

	@RequestMapping(value="/updateComment", method=RequestMethod.POST)
	public String updateComment(Integer commentNo, 
			Integer articleNo, 
			String boardCd, 
			Integer page, 
			String searchWord, 
			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);

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

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

	}

	@RequestMapping(value="/deleteComment", method=RequestMethod.POST)
	public String deleteComment(Integer commentNo, 
			Integer articleNo, 
			String boardCd, 
			Integer page, 
			String searchWord,
			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);

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

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

	}

	@RequestMapping(value="/modify", method=RequestMethod.GET)
	public String modifyForm(Integer articleNo, 
			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";
	}

	@RequestMapping(value="/modify", method=RequestMethod.POST)
	public String modify(MultipartHttpServletRequest mpRequest,
			HttpSession session) throws Exception {

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

		int articleNo = Integer.parseInt(mpRequest.getParameter("articleNo"));
		Article article = boardService.getArticle(articleNo);

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

		String boardCd = mpRequest.getParameter("boardCd");
		int page = Integer.parseInt(mpRequest.getParameter("page"));
		String searchWord = mpRequest.getParameter("searchWord");

		String title = mpRequest.getParameter("title");
		String content = mpRequest.getParameter("content");

		article.setTitle(title);
		article.setContent(content);
		article.setBoardCd(boardCd);
		boardService.modifyArticle(article);

		Iterator<String> it = mpRequest.getFileNames();
		List<MultipartFile> fileList = new ArrayList<MultipartFile>();
		while (it.hasNext()) {
			MultipartFile multiFile = mpRequest.getFile((String) it.next());
			if (multiFile.getSize() > 0) {
				String filename = multiFile.getOriginalFilename();
				multiFile.transferTo(new File(WebContants.UPLOAD_PATH + filename));
				fileList.add(multiFile);
			}
		}

		int size = fileList.size();
		for (int i = 0; i < size; i++) {
			MultipartFile mpFile = fileList.get(i);
			AttachFile attachFile = new AttachFile();
			String filename = mpFile.getOriginalFilename();
			attachFile.setFilename(filename);
			attachFile.setFiletype(mpFile.getContentType());
			attachFile.setFilesize(mpFile.getSize());
			attachFile.setArticleNo(articleNo);
			attachFile.setEmail(user.getEmail());
			boardService.addAttachFile(attachFile);
		}

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

	}

	@RequestMapping(value="/download", method=RequestMethod.POST)
	public String download(String filename, HttpSession session, Model model) {
		User user = (User) session.getAttribute(WebContants.USER_KEY);
		if (user == null) {
			throw new AuthenticationException(WebContants.NOT_LOGIN);
		}

		model.addAttribute("filename", filename);
		return "inc/download";

	}

	@RequestMapping(value="/deleteAttachFile", method=RequestMethod.POST)
	public String deleteAttachFile(Integer attachFileNo, 
			Integer articleNo, 
			String boardCd, 
			Integer page, 
			String searchWord,
			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);

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

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

	}

	@RequestMapping(value="/del", method=RequestMethod.POST)
	public String del(Integer articleNo, 
			String boardCd, 
			Integer page, 
			String searchWord,
			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);

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

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

	}

}

The handler method parameters boardCd, page, and searchWord are assigned the values of the request parameters.
If the request parameter name is curPage and the parameter name is page, resolve as follows.

@RequestParam("curPage") String page

The writeForm() method is a handler for the /bbs/write request of the GET method.

@RequestMapping(value="write", method=RequestMethod.GET)
public String writeForm(String boardCd,HttpServletRequest req,HttpSession session...)

The write() method handles the /bbs/write request of the POST method.

@RequestMapping(value="write", method=RequestMethod.POST)
public String write(MultipartHttpServletRequest mpRequest, HttpSession session) throws Exception

The mpRequest parameter of the MultipartHttpServletRequest type has access to the files passed to the system.

The write() method first checks the login.
If the user is not logged in, AuthenticationException exception occurs and the view resolver selects /WEB-INF/views/error.jsp as the view.

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

If a login check is passed, a new entry is inserted into the table.
When boardService.addArticle (article) is executed, the article's setArticleNo() is called to set the article's unique number.

String boardCd = mpRequest.getParameter("boardCd");
String title = mpRequest.getParameter("title");
String content = mpRequest.getParameter("content");

Article article = new Article();
article.setBoardCd(boardCd);
article.setTitle(title);
article.setContent(content);
article.setEmail(user.getEmail());

boardService.addArticle(article);

After inserting the new post information into the table, move the uploaded file to the specified directory.
The writing form page can only upload an attachment, but the handler method is implemented to handle multiple upload files.

Iterator<String> it = mpRequest.getFileNames();
List<MultipartFile> fileList = new ArrayList<MultipartFile>();
while (it.hasNext()) {
    MultipartFile multiFile = mpRequest.getFile((String) it.next());
    if (multiFile.getSize() > 0) {
        String filename = multiFile.getOriginalFilename();
        multiFile.transferTo(new File(WebContants.UPLOAD_PATH + filename));
        fileList.add(multiFile);
    }
}

The above code shows how to handle uploaded files.
The MultipartHttpServletRequest type has access to the files passed to the system.

Spring MVC supports Apache's commons-fileupload for file uploads.
The developer has to add the necessary dependencies and set the multipartResolver bean in the Spring configuration file.

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.3.2</version>
</dependency>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  <property name="maxUploadSize" value="2097152" />
</bean>

The method moves the attachment to the upload directory and inserts the file information into the table.
article.getArticleNo() gets the unique number of the article that is inserted into the table.

int size = fileList.size();
for (int i = 0; i < size; i++) {
    MultipartFile mpFile = fileList.get(i);
    AttachFile attachFile = new AttachFile();
    String filename = mpFile.getOriginalFilename();
    attachFile.setFilename(filename);
    attachFile.setFiletype(mpFile.getContentType());
    attachFile.setFilesize(mpFile.getSize());
    attachFile.setArticleNo(article.getArticleNo());
    attachFile.setEmail(user.getEmail());
    boardService.addAttachFile(attachFile);
}

Two new methods are used: addArticle(article) and addAttachFile(attachFile).
(Note that in the Model 2 board, the addArticle(article, attachFile) method is used)

When using a redirect, you need to add the boardCd and page=1 to the request as shown below.

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

Detailed view

The following method is a handler for /bbs/view request.

@RequestMapping(value="/view", method=RequestMethod.GET)
public String view(Integer articleNo, 
        String boardCd, 
        Integer page,
        String searchWord,
        HttpServletRequest req,
        HttpSession session,
        Model model) throws Exception {

The method first checks the login.
If the login check is passed, the number of views is increased and the necessary data is produced on the detailed view screen.

boardService.increaseHit(articleNo);

Article article = boardService.getArticle(articleNo);
List<AttachFile> attachFileList = boardService.getAttachFileList(articleNo);
Article nextArticle = boardService.getNextArticle(articleNo, boardCd, searchWord);
Article prevArticle = boardService.getPrevArticle(articleNo, boardCd, searchWord);
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 request of POST method.

@RequestMapping(value="/addComment", method=RequestMethod.POST)
public String addComment(Integer articleNo, 
        String boardCd, 
        Integer page, 
        String searchWord,
        String memo,
        HttpSession session) throws Exception {

That method first check login.

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

If login check passes, Comment information be inserted to the table with request parameters.

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

boardService.addComment(comment);

The search term be encoded.

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

The method gets your web browser to redirect the request to view.jsp.

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

Modify comment

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

@RequestMapping(value="/updateComment", method=RequestMethod.POST)
public String updateComment(
        Integer commentNo, 
        Integer articleNo, 
        String boardCd, 
        Integer page, 
        String searchWord, 
        String memo,
        HttpSession session) throws Exception {

This method first checks whether the user is logged in and the owner of the text, and throws a custom authentication exception if it does not pass the check.

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 passed, this method modifies the comment with the request parameters.
Only the content of the comment can be edited, and the original owner of the comment will not change, even if the administrator modifies the comment.

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

The method gets your web browser to redirect the request to view.jsp.

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

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

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

@RequestMapping(value="/deleteComment", method=RequestMethod.POST)
public String deleteComment(
        Integer commentNo, 
        Integer articleNo, 
        String boardCd, 
        Integer page, 
        String searchWord,
        HttpSession session) throws Exception {

This method first checks whether the user is logged in and the owner of the text, and throws a custom authentication exception if it does not pass the check.

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 passed, this method deletes the comment with the request parameter.

boardService.removeComment(commentNo);

The mehtod gets your web browser to redirect the request to view.jsp.

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

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

The download() is a handle for '/bbs/download' request of POST method.

@RequestMapping(value="/download", method=RequestMethod.POST)
public String download(String filename, HttpSession session, Model model) {

The method passes the filename to download.jsp.

model.addAttribute("filename", filename);
return "inc/download";

Modify view.jsp

Add the following javascript code to the view.jsp.

function download(filename) {
    var form = document.getElementById("downForm");
    form.filename.value = filename;
    form.submit();
}

Add the following form to the #form-group.

<form id="downForm" action="download" method="post">
    <input type="hidden" name="filename" />
</form>

Modify file download links as shown below.

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

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/spring-bbs/src/main/webapp"
    reloadable="true">
</Context>

Restart Tomcat.
Visit 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>

The next section covers Spring Security.
Spring Security is an authentication framework.

If an error occurs during testing, you need to check the log file set in log4j.xml or the log file in CATALINA_HOME/logs.
No one will understand all of the log messages.
You should develop your ability to see the log and infer the cause of the error.

References