JSP MVCモデルを使った模擬アプリ課題

2025-08-05

以下に、Servlet、JSP、JDBC、フロントコントローラーパターンを活用した3つの課題を提案します。

課題1: 簡易タスク管理アプリケーション

要件

  • タスクの一覧表示、登録、更新、削除ができる
  • フロントコントローラーパターンを適用
  • データベースはMySQLを使用
  • タスクには「タイトル」「内容」「優先度」「期限」を含める

模範解答コード

1. データベース接続用クラス (DatabaseConnector.java)

public class DatabaseConnector {
    private static final String URL = "jdbc:mysql://localhost:3306/taskdb";
    private static final String USER = "root";
    private static final String PASS = "password";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASS);
    }
}

2. モデルクラス (Task.java)

public class Task {
    private int id;
    private String title;
    private String content;
    private String priority;
    private LocalDate deadline;

    // コンストラクタ、ゲッター、セッター省略
}

3. DAOクラス (TaskDAO.java)

public class TaskDAO {
    public List findAll() throws SQLException {
        List tasks = new ArrayList<>();
        String sql = "SELECT * FROM tasks ORDER BY deadline ASC";

        try (Connection conn = DatabaseConnector.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql);
             ResultSet rs = stmt.executeQuery()) {

            while (rs.next()) {
                Task task = new Task();
                task.setId(rs.getInt("id"));
                task.setTitle(rs.getString("title"));
                task.setContent(rs.getString("content"));
                task.setPriority(rs.getString("priority"));
                task.setDeadline(rs.getDate("deadline").toLocalDate());
                tasks.add(task);
            }
        }
        return tasks;
    }

    // 他のCRUDメソッドも同様に実装
}

4. フロントコントローラー (FrontController.java)

@WebServlet("*.do")
public class FrontController extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String path = request.getServletPath();

        switch(path) {
            case "/list.do":
                new ListTaskController().execute(request, response);
                break;
            case "/add.do":
                new AddTaskController().execute(request, response);
                break;
            // 他のケースも追加
            default:
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        doGet(request, response);
    }
}

5. コントローラーの例 (ListTaskController.java)

public class ListTaskController implements Controller {
    @Override
    public void execute(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        try {
            TaskDAO dao = new TaskDAO();
            List tasks = dao.findAll();
            request.setAttribute("tasks", tasks);
            request.getRequestDispatcher("/WEB-INF/view/list.jsp").forward(request, response);
        } catch (SQLException e) {
            throw new ServletException(e);
        }
    }
}

6. JSP (list.jsp)

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>タスク一覧</title>
</head>
<body>
    <h1>タスク一覧</h1>
    <table border="1">
        <tr>
            <th>タイトル</th>
            <th>優先度</th>
            <th>期限</th>
            <th>操作</th>
        </tr>
        <c:forEach items="${tasks}" var="task">
            <tr>
                <td>${task.title}</td>
                <td>${task.priority}</td>
                <td>${task.deadline}</td>
                <td>
                    <a href="edit.do?id=${task.id}">編集</a>
                    <a href="delete.do?id=${task.id}">削除</a>
                </td>
            </tr>
        </c:forEach>
    </table>
    <a href="add.do">新規追加</a>
</body>
</html>

課題2: ユーザー認証システム

要件

  • ユーザー登録、ログイン、ログアウト機能
  • セッション管理による認証状態の維持
  • パスワードはハッシュ化して保存
  • 認証が必要なページと不要なページの区別

模範解答コード

1. ユーザーモ型 (User.java)

public class User {
    private int id;
    private String username;
    private String passwordHash;
    private String email;

    // コンストラクタ、ゲッター、セッター省略
}

2. 認証フィルター (AuthFilter.java)

@WebFilter("/*")
public class AuthFilter implements Filter {
    private static final List ALLOWED_PATHS = Arrays.asList(
        "/login.do", "/register.do", "/css/", "/js/");

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String path = request.getRequestURI().substring(request.getContextPath().length());

        boolean allowedPath = ALLOWED_PATHS.stream().anyMatch(path::startsWith);

        if (allowedPath || request.getSession().getAttribute("user") != null) {
            chain.doFilter(request, response);
        } else {
            response.sendRedirect(request.getContextPath() + "/login.do");
        }
    }

    // init, destroyメソッド省略
}

3. ログインコントローラー (LoginController.java)

public class LoginController implements Controller {
    @Override
    public void execute(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        if ("GET".equals(request.getMethod())) {
            request.getRequestDispatcher("/WEB-INF/view/login.jsp").forward(request, response);
            return;
        }

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        try {
            UserDAO userDAO = new UserDAO();
            User user = userDAO.findByUsername(username);

            if (user != null && PasswordUtil.verify(password, user.getPasswordHash())) {
                request.getSession().setAttribute("user", user);
                response.sendRedirect(request.getContextPath() + "/home.do");
            } else {
                request.setAttribute("error", "ユーザー名またはパスワードが不正です");
                request.getRequestDispatcher("/WEB-INF/view/login.jsp").forward(request, response);
            }
        } catch (SQLException e) {
            throw new ServletException(e);
        }
    }
}

4. パスワードユーティリティ (PasswordUtil.java)

public class PasswordUtil {
    private static final int ITERATIONS = 10000;
    private static final int KEY_LENGTH = 256;
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    public static String hash(String password) {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);

        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
            byte[] hash = factory.generateSecret(spec).getEncoded();
            return ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean verify(String password, String storedHash) {
        String[] parts = storedHash.split(":");
        int iterations = Integer.parseInt(parts[0]);
        byte[] salt = fromHex(parts[1]);
        byte[] hash = fromHex(parts[2]);

        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, hash.length * 8);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
            byte[] testHash = factory.generateSecret(spec).getEncoded();
            return Arrays.equals(hash, testHash);
        } catch (Exception e) {
            return false;
        }
    }

    private static String toHex(byte[] array) {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if (paddingLength > 0) {
            return String.format("%0" + paddingLength + "d", 0) + hex;
        } else {
            return hex;
        }
    }

    private static byte[] fromHex(String hex) {
        byte[] bytes = new byte[hex.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return bytes;
    }
}

課題3: 商品管理システム

要件

  • 商品のCRUD操作
  • カテゴリ別商品一覧
  • ページネーション機能
  • 検索機能(商品名、価格範囲)

模範解答コード

1. 商品DAO (ProductDAO.java)


public class ProductDAO {
    public List<Product> findByCriteria(ProductSearchCriteria criteria) throws SQLException {
        List<Product> products = new ArrayList<>();
        StringBuilder sql = new StringBuilder("SELECT * FROM products WHERE 1=1 ");
        List<Object> params = new ArrayList<>();
        
        if (criteria.getName() != null && !criteria.getName().isEmpty()) {
            sql.append("AND name LIKE ? ");
            params.add("%" + criteria.getName() + "%");
        }
        
        if (criteria.getCategoryId() != null) {
            sql.append("AND category_id = ? ");
            params.add(criteria.getCategoryId());
        }
        
        if (criteria.getMinPrice() != null) {
            sql.append("AND price >= ? ");
            params.add(criteria.getMinPrice());
        }
        
        if (criteria.getMaxPrice() != null) {
            sql.append("AND price <= ? ");
            params.add(criteria.getMaxPrice());
        }
        
        sql.append("ORDER BY ").append(criteria.getSortField())
           .append(" ").append(criteria.getSortDirection())
           .append(" LIMIT ? OFFSET ?");
        
        params.add(criteria.getLimit());
        params.add(criteria.getOffset());
        
        try (Connection conn = DatabaseConnector.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
            
            for (int i = 0; i < params.size(); i++) {
                stmt.setObject(i + 1, params.get(i));
            }
            
            try (ResultSet rs = stmt.executeQuery()) {
                while (rs.next()) {
                    Product product = new Product();
                    product.setId(rs.getInt("id"));
                    product.setName(rs.getString("name"));
                    product.setPrice(rs.getInt("price"));
                    product.setStock(rs.getInt("stock"));
                    product.setCategoryId(rs.getInt("category_id"));
                    products.add(product);
                }
            }
        }
        return products;
    }
    
    public int countByCriteria(ProductSearchCriteria criteria) throws SQLException {
        // 同様の検索条件でCOUNTを取得
    }
}

2. 検索条件クラス (ProductSearchCriteria.java)

public class ProductSearchCriteria {
    private String name;
    private Integer categoryId;
    private Integer minPrice;
    private Integer maxPrice;
    private String sortField = "id";
    private String sortDirection = "ASC";
    private int page = 1;
    private int limit = 10;

    public int getOffset() {
        return (page - 1) * limit;
    }

    // ゲッター、セッター省略
}

3. 商品リストコントローラー (ListProductController.java)


public class ListProductController implements Controller {
    @Override
    public void execute(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        ProductSearchCriteria criteria = new ProductSearchCriteria();

        try {
            // リクエストパラメータから検索条件を設定
            if (request.getParameter("name") != null) {
                criteria.setName(request.getParameter("name"));
            }
            if (request.getParameter("categoryId") != null) {
                criteria.setCategoryId(Integer.parseInt(request.getParameter("categoryId")));
            }
            if (request.getParameter("minPrice") != null) {
                criteria.setMinPrice(Integer.parseInt(request.getParameter("minPrice")));
            }
            if (request.getParameter("maxPrice") != null) {
                criteria.setMaxPrice(Integer.parseInt(request.getParameter("maxPrice")));
            }
            if (request.getParameter("page") != null) {
                criteria.setPage(Integer.parseInt(request.getParameter("page")));
            }

            ProductDAO productDAO = new ProductDAO();
            List products = productDAO.findByCriteria(criteria);
            int totalCount = productDAO.countByCriteria(criteria);

            request.setAttribute("products", products);
            request.setAttribute("criteria", criteria);
            request.setAttribute("totalPages", (int) Math.ceil((double) totalCount / criteria.getLimit()));

            request.getRequestDispatcher("/WEB-INF/view/product/list.jsp").forward(request, response);

        } catch (SQLException | NumberFormatException e) {
            throw new ServletException(e);
        }
    }
}

4. 商品リストJSP (list.jsp)

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>商品一覧</title>
</head>
<body>
    <h1>商品一覧</h1>

    <form method="get" action="list.do">
        <input type="text" name="name" placeholder="商品名" value="${criteria.name}">
        <select name="categoryId">
            <option value="">すべてのカテゴリ</option>
            <c:forEach items="${categories}" var="category">
                <option value="${category.id}" 
                    ${criteria.categoryId == category.id ? 'selected' : ''}>
                    ${category.name}
                </option>
            </c:forEach>
        </select>
        <input type="number" name="minPrice" placeholder="最低価格" value="${criteria.minPrice}">
        <input type="number" name="maxPrice" placeholder="最高価格" value="${criteria.maxPrice}">
        <button type="submit">検索</button>
    </form>

    <table border="1">
        <tr>
            <th>ID</th>
            <th>商品名</th>
            <th>価格</th>
            <th>在庫</th>
            <th>操作</th>
        </tr>
        <c:forEach items="${products}" var="product">
            <tr>
                <td>${product.id}</td>
                <td>${product.name}</td>
                <td>${product.price}</td>
                <td>${product.stock}</td>
                <td>
                    <a href="edit.do?id=${product.id}">編集</a>
                    <a href="delete.do?id=${product.id}">削除</a>
                </td>
            </tr>
        </c:forEach>
    </table>

    <div class="pagination">
        <c:if test="${criteria.page > 1}">
            <a href="list.do?page=${criteria.page - 1}&name=${criteria.name}&categoryId=${criteria.categoryId}">前へ</a>
        </c:if>

        <c:forEach begin="1" end="${totalPages}" var="i">
            <c:choose>
                <c:when test="${i == criteria.page}">
                    <strong>${i}</strong>
                </c:when>
                <c:otherwise>
                    <a href="list.do?page=${i}&name=${criteria.name}&categoryId=${criteria.categoryId}">${i}</a>
                </c:otherwise>
            </c:choose>
        </c:forEach>

        <c:if test="${criteria.page < totalPages}">
            <a href="list.do?page=${criteria.page + 1}&name=${criteria.name}&categoryId=${criteria.categoryId}">次へ</a>
        </c:if>
    </div>

    <a href="add.do">新規追加</a>
</body>
</html>

これらの課題を通じて、JSPのMVCモデルにおける各コンポーネントの役割と連携を実践的に学ぶことができます。データベース設計やエラーハンドリングなど、さらに拡張できるポイントも多くありますので、必要に応じて機能を追加してみてください。