Board program on Spring MVC

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

Source

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

Database design

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

Copy files

To keep the model 2 source, we will put the spring bulletin board project in the spring-bbs project in the previous section.

Stylesheets and Images

Copy the /css and /images folders from C:/www/JSPProject/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 JSPs in the /WEB-INF subdirectory is to prevent the web browser from directly accessing them.
Spring MVC recommends a system where the dispatcher servlet receives all requests and passes them back to the controller.

Java sources

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

logging configuration file

Copy the following commons-logging.properties and log4j2.xml from JSPProject and paste them into 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>

Mapping dispatcher servlet

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>

Modify JSPs

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.

Using date format in list.jsp and view.jsp

JSTL makes it easy to format dates in JSPs.

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 JSP used to download attachments

So far, we have downloaded the linked attachments. In the Spring MVC project, let's create and use a JSP that downloads attachments.

/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();
%>

With download.jsp, you can download attachments even if they are in a location that your web browser can not access.

In the above, WebContants.UPLOAD_PATH is the directory where the bulletin board system saves attachment files.

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 cannot access. Therefore, you cannot make links in attachments in this directory. You need to change the attachment download code in the view.jsp.

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.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 make 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. You need to create configuration files in src/main/resources. When you make configuration files in this directory, Maven copies them to the classpath during the build process.

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 exact location as Configuration.xml. In the mapper file, id values must match method names 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:/";

  }

}

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

@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";
  }
}

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.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";
  }

}

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

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

  @RequestMapping(method=RequestMethod.GET)
  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 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>
<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.mybatis.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 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>

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.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 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 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 BbsController.java 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;
  }

}

A handler method parameters like boardCd, page, and searchWord 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 curPage, you should declare the handler method parameter, page like below.

@RequestParam("curPage") String page

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

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

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

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

You can access files passed to the system using the mpRequest parameter of the MultipartHttpServletRequest type.

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/error.jsp as the view.

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.

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 into the table, Our code starts to handle an uploaded file.

You can upload only one attachment with the writing form page, but the following handler method can 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);
  }
}

Spring MVC supports Apache's commons-fileupload, which is a framework for file uploads. Developers just need 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.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.4</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. The code, article.getArticleNo(), gets the unique number of the post from 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);
}

The code above uses two new methods, addArticle(article) and addAttachFile(attachFile), to insert attachment information into the table. 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.

@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 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, 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 requests 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 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.

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

The method redirects the request to view.jsp.

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

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 {

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.

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

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 requests of the POST method.

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

The method passes a 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

C:\ Command Prompt
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 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>

If an error occurs during testing, you need to check the log file set in log4j2.xml or the log file in CATALINA_HOME/logs. It is not necessary to understand all the log messages. However, you should develop the ability to look at log messages and deduce the cause of the error.

References