From 6164a3b6f2975e7ca86df40a42276cab9557a0d3 Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 26 Feb 2026 17:48:48 +0300 Subject: [PATCH 1/3] ... --- src/main/java/bakhtin/BasicsServlet.java | 35 +++++ src/main/java/bakhtin/FrontController.java | 23 +++ src/main/java/bakhtin/HelloServlet.java | 44 ++++++ src/main/webapp/WEB-INF/basics.jsp | 157 +++++++++++++++++++++ src/main/webapp/WEB-INF/hello-result.jsp | 27 ++++ src/main/webapp/WEB-INF/hello.jsp | 50 +++++++ 6 files changed, 336 insertions(+) create mode 100644 src/main/java/bakhtin/BasicsServlet.java create mode 100644 src/main/java/bakhtin/FrontController.java create mode 100644 src/main/java/bakhtin/HelloServlet.java create mode 100644 src/main/webapp/WEB-INF/basics.jsp create mode 100644 src/main/webapp/WEB-INF/hello-result.jsp create mode 100644 src/main/webapp/WEB-INF/hello.jsp diff --git a/src/main/java/bakhtin/BasicsServlet.java b/src/main/java/bakhtin/BasicsServlet.java new file mode 100644 index 0000000..2abae85 --- /dev/null +++ b/src/main/java/bakhtin/BasicsServlet.java @@ -0,0 +1,35 @@ +package bakhtin; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Сервлет, демонстрирующий основные концепции сервлетов и JSP + */ +@WebServlet("/basics") +public class BasicsServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // Примеры данных разных типов + List concepts = new ArrayList<>(); + concepts.add("Сервлет обрабатывает HTTP запросы"); + concepts.add("JSP генерирует HTML ответ"); + concepts.add("request - объект с данными запроса"); + concepts.add("response - объект для отправки ответа"); + concepts.add("session - хранилище данных пользователя между запросами"); + + request.setAttribute("concepts", concepts); + request.setAttribute("currentTime", System.currentTimeMillis()); + + request.getRequestDispatcher("/WEB-INF/basics.jsp").forward(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/bakhtin/FrontController.java b/src/main/java/bakhtin/FrontController.java new file mode 100644 index 0000000..4d8c2a3 --- /dev/null +++ b/src/main/java/bakhtin/FrontController.java @@ -0,0 +1,23 @@ +package bakhtin; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Главный контроллер для папки bakhtin + * Перенаправляет на страницу с примерами + */ +@WebServlet({"/bakhtin", "/bakhtin/", "/bakhtin/home"}) +public class FrontController extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Перенаправляем на страницу basics с объяснениями + req.getRequestDispatcher("/WEB-INF/basics.jsp").forward(req, resp); + } +} diff --git a/src/main/java/bakhtin/HelloServlet.java b/src/main/java/bakhtin/HelloServlet.java new file mode 100644 index 0000000..5560a8c --- /dev/null +++ b/src/main/java/bakhtin/HelloServlet.java @@ -0,0 +1,44 @@ +package bakhtin; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Простой сервлет, который обрабатывает GET запросы + * и передает управление на JSP страницу + */ +@WebServlet("/hello") +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // 1. Подготовка данных для передачи в JSP + String message = "Привет из сервлета!"; + String userName = "Пользователь"; + + // 2. Помещаем данные в request - так JSP сможет их прочитать + request.setAttribute("message", message); + request.setAttribute("userName", userName); + + // 3. Forward - передаем управление на JSP страницу + // URL в браузере останется /hello, но контент будет от JSP + request.getRequestDispatcher("/WEB-INF/hello.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // Пример обработки POST запроса (отправка формы) + String name = request.getParameter("name"); + request.setAttribute("submittedName", name != null ? name : "Гость"); + + request.getRequestDispatcher("/WEB-INF/hello-result.jsp").forward(request, response); + } +} \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/basics.jsp b/src/main/webapp/WEB-INF/basics.jsp new file mode 100644 index 0000000..80647b9 --- /dev/null +++ b/src/main/webapp/WEB-INF/basics.jsp @@ -0,0 +1,157 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + Основы сервлетов и JSP + + + +
+

Основы сервлетов и JSP

+ + +
+
+

Основные концепции

+
+
+
    + +
  • ✓ ${concept}
  • +
    +
+
+
+ + +
+
+

Структура сервлета

+
+
+
@WebServlet("/url")
+public class MyServlet extends HttpServlet {
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        // Обработка GET запроса
+
+        // 1. Подготовка данных
+        req.setAttribute("key", "value");
+
+        // 2. Forward на JSP
+        req.getRequestDispatcher("/WEB-INF/page.jsp")
+           .forward(req, resp);
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        // Обработка POST запроса (форма)
+
+        // 1. Получение данных из формы
+        String param = req.getParameter("paramName");
+
+        // 2. Обработка...
+
+        // 3. Redirect (изменяет URL в браузере)
+        resp.sendRedirect("/success");
+    }
+}
+
+
+ + +
+
+

JSP - основные конструкции

+
+
+
1. JSP EL (Expression Language) - чтение данных
+
${attributeName}           // Атрибут request/session/application
+${param.paramName}        // Параметр запроса
+${requestScope.user.name} // Свойство объекта
+${1 + 2}                 // Выражения: 3
+${user.name != null}     // Условия
+ +
2. JSTL - теги для логики
+
<%@ taglib uri="..." prefix="c" %>
+
+<c:forEach var="item" items="${list}">
+    ${item.name}
+</c:forEach>
+
+<c:if test="${user.admin}">
+    <a href="/admin">Админка</a>
+</c:if>
+ +
3. Скриптлеты (устаревший способ)
+
<% String msg = "Привет"; %>
+<%= msg %>
+
+ ⚠️ Не используйте скриптлеты! Используйте EL и JSTL вместо них. +
+
+
+ + +
+
+

Forward vs Redirect

+
+
+
+
+
Forward
+
    +
  • Остаётся на том же URL
  • +
  • Данные request передаются
  • +
  • Быстрее (один запрос)
  • +
  • request.getRequestDispatcher().forward()
  • +
+
// URL: /hello
+// Контент: /WEB-INF/hello.jsp
+req.getRequestDispatcher("/WEB-INF/page.jsp")
+   .forward(req, resp);
+
+
+
Redirect
+
    +
  • URL меняется в браузере
  • +
  • Данные request теряются
  • +
  • Два запроса (два HTTP ответа)
  • +
  • response.sendRedirect()
  • +
+
// URL: /success
+// Контент: /WEB-INF/success.jsp
+resp.sendRedirect("/success");
+
+
+
+
+ + +
+
+

Попробовать примеры

+
+
+ +

+ Время генерации страницы: ${currentTime} +

+
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/hello-result.jsp b/src/main/webapp/WEB-INF/hello-result.jsp new file mode 100644 index 0000000..fdc4ffd --- /dev/null +++ b/src/main/webapp/WEB-INF/hello-result.jsp @@ -0,0 +1,27 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Результат формы + + + +
+
+
+

Результат отправки формы

+ +
+

Привет, ${submittedName}!

+

Это данные, которые пришли из формы через POST запрос.

+
+ +
+ + ← Вернуться назад +
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/hello.jsp b/src/main/webapp/WEB-INF/hello.jsp new file mode 100644 index 0000000..95b2efe --- /dev/null +++ b/src/main/webapp/WEB-INF/hello.jsp @@ -0,0 +1,50 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Базовая страница + + + + +
+
+
+

Базовая страница на сервлетах

+ +
+ + +

${message}

+

Добро пожаловать, ${userName}!

+ +
+ + +

Тестовая форма

+
+
+ + +
+ +
+ +
+ +
+ Как это работает: +
    +
  • Сервлет обрабатывает запрос по URL /hello
  • +
  • Сервлет готовит данные и кладет их в request.setAttribute()
  • +
  • Сервлет делает forward на JSP страницу
  • +
  • JSP использует ${attributeName} для чтения данных
  • +
+
+
+
+
+ + \ No newline at end of file From 4d6647c2195cf83ba01a2f793d3ec018eeb2e439 Mon Sep 17 00:00:00 2001 From: Denis Date: Tue, 10 Mar 2026 17:49:05 +0300 Subject: [PATCH 2/3] ... --- pom.xml | 11 + src/main/java/bakhtin/Actions.java | 7 + src/main/java/bakhtin/BasicsServlet.java | 35 --- .../java/bakhtin/ErrorHandlingFilter.java | 37 ++++ src/main/java/bakhtin/FrontController.java | 9 +- src/main/java/bakhtin/HelloServlet.java | 17 +- src/main/java/bakhtin/Quest.java | 208 ++++++++++++++++++ src/main/java/bakhtin/QuestServlet.java | 189 ++++++++++++++++ .../IllegalActionException.java" | 8 + .../NoActiveQuestException.java" | 8 + .../NoAnswerGivenException.java" | 8 + .../NoQuestionException.java" | 8 + .../ex\321\201eptions/QuestException.java" | 8 + .../khmelov/controller/FrontController.java | 2 +- src/main/webapp/WEB-INF/basics.jsp | 157 ------------- src/main/webapp/WEB-INF/hello-result.jsp | 27 --- src/main/webapp/WEB-INF/hello.jsp | 54 ++--- src/main/webapp/WEB-INF/quest.jsp | 95 ++++++++ src/test/java/bakhtin/ActionsTest.java | 22 ++ src/test/java/bakhtin/QuestServletTest.java | 63 ++++++ src/test/java/bakhtin/QuestTest.java | 36 +++ 21 files changed, 738 insertions(+), 271 deletions(-) create mode 100644 src/main/java/bakhtin/Actions.java delete mode 100644 src/main/java/bakhtin/BasicsServlet.java create mode 100644 src/main/java/bakhtin/ErrorHandlingFilter.java create mode 100644 src/main/java/bakhtin/Quest.java create mode 100644 src/main/java/bakhtin/QuestServlet.java create mode 100644 "src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" create mode 100644 "src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" create mode 100644 "src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" create mode 100644 "src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" create mode 100644 "src/main/java/bakhtin/ex\321\201eptions/QuestException.java" delete mode 100644 src/main/webapp/WEB-INF/basics.jsp delete mode 100644 src/main/webapp/WEB-INF/hello-result.jsp create mode 100644 src/main/webapp/WEB-INF/quest.jsp create mode 100644 src/test/java/bakhtin/ActionsTest.java create mode 100644 src/test/java/bakhtin/QuestServletTest.java create mode 100644 src/test/java/bakhtin/QuestTest.java diff --git a/pom.xml b/pom.xml index 78ee59d..2aa0e92 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,17 @@ junit-jupiter-engine test + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-junit-jupiter + 5.21.0 + test + diff --git a/src/main/java/bakhtin/Actions.java b/src/main/java/bakhtin/Actions.java new file mode 100644 index 0000000..17a3cf9 --- /dev/null +++ b/src/main/java/bakhtin/Actions.java @@ -0,0 +1,7 @@ +package bakhtin; + +public enum Actions { + ANSWER, + RESTART, + EXIT +} diff --git a/src/main/java/bakhtin/BasicsServlet.java b/src/main/java/bakhtin/BasicsServlet.java deleted file mode 100644 index 2abae85..0000000 --- a/src/main/java/bakhtin/BasicsServlet.java +++ /dev/null @@ -1,35 +0,0 @@ -package bakhtin; - -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Сервлет, демонстрирующий основные концепции сервлетов и JSP - */ -@WebServlet("/basics") -public class BasicsServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // Примеры данных разных типов - List concepts = new ArrayList<>(); - concepts.add("Сервлет обрабатывает HTTP запросы"); - concepts.add("JSP генерирует HTML ответ"); - concepts.add("request - объект с данными запроса"); - concepts.add("response - объект для отправки ответа"); - concepts.add("session - хранилище данных пользователя между запросами"); - - request.setAttribute("concepts", concepts); - request.setAttribute("currentTime", System.currentTimeMillis()); - - request.getRequestDispatcher("/WEB-INF/basics.jsp").forward(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/bakhtin/ErrorHandlingFilter.java b/src/main/java/bakhtin/ErrorHandlingFilter.java new file mode 100644 index 0000000..34673f6 --- /dev/null +++ b/src/main/java/bakhtin/ErrorHandlingFilter.java @@ -0,0 +1,37 @@ +package bakhtin; + +import bakhtin.exсeptions.IllegalActionException; +import bakhtin.exсeptions.NoActiveQuestException; +import bakhtin.exсeptions.NoAnswerGivenException; +import bakhtin.exсeptions.NoQuestionException; +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import java.io.IOException; + +@WebFilter("/*") +public class ErrorHandlingFilter implements Filter { + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException { + try { + chain.doFilter(req, resp); + } catch (NoActiveQuestException e) { + // TODO(log): log.error("No active quest exception", e); + req.setAttribute("errorMessage", "Квест не найден. Начните квест заново."); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } catch (NoAnswerGivenException e) { + // TODO(log): log.error("No answer given exception", e); + req.setAttribute("errorMessage", "Не выбран ответ. Попробуйте ещё раз."); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } catch (NoQuestionException e) { + // TODO(log): log.error("No question exception", e); + req.setAttribute("errorMessage", "Вопрос не найден. Начните квест заново."); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } catch (IllegalActionException e) { + // TODO(log): log.error("Illegal action exception", e); + req.setAttribute("errorMessage", "Неверное действие. Попробуйте ещё раз"); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } + } +} \ No newline at end of file diff --git a/src/main/java/bakhtin/FrontController.java b/src/main/java/bakhtin/FrontController.java index 4d8c2a3..3ed5c4e 100644 --- a/src/main/java/bakhtin/FrontController.java +++ b/src/main/java/bakhtin/FrontController.java @@ -1,23 +1,26 @@ package bakhtin; +import bakhtin.Quest.Question.Answer; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Главный контроллер для папки bakhtin * Перенаправляет на страницу с примерами */ -@WebServlet({"/bakhtin", "/bakhtin/", "/bakhtin/home"}) +@WebServlet({"/bakhtin", "/bakhtin/", "/bakhtin/home", ""}) public class FrontController extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // Перенаправляем на страницу basics с объяснениями - req.getRequestDispatcher("/WEB-INF/basics.jsp").forward(req, resp); + // Перенаправляем на quest сервлет + resp.sendRedirect(req.getContextPath() + "/hello"); } } diff --git a/src/main/java/bakhtin/HelloServlet.java b/src/main/java/bakhtin/HelloServlet.java index 5560a8c..d07e943 100644 --- a/src/main/java/bakhtin/HelloServlet.java +++ b/src/main/java/bakhtin/HelloServlet.java @@ -18,27 +18,16 @@ public class HelloServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - // 1. Подготовка данных для передачи в JSP + // Подготовка данных для передачи в JSP String message = "Привет из сервлета!"; String userName = "Пользователь"; - // 2. Помещаем данные в request - так JSP сможет их прочитать + // Помещаем данные в request - так JSP сможет их прочитать request.setAttribute("message", message); request.setAttribute("userName", userName); - // 3. Forward - передаем управление на JSP страницу + // Forward - передаем управление на JSP страницу // URL в браузере останется /hello, но контент будет от JSP request.getRequestDispatcher("/WEB-INF/hello.jsp").forward(request, response); } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // Пример обработки POST запроса (отправка формы) - String name = request.getParameter("name"); - request.setAttribute("submittedName", name != null ? name : "Гость"); - - request.getRequestDispatcher("/WEB-INF/hello-result.jsp").forward(request, response); - } } \ No newline at end of file diff --git a/src/main/java/bakhtin/Quest.java b/src/main/java/bakhtin/Quest.java new file mode 100644 index 0000000..47bd05f --- /dev/null +++ b/src/main/java/bakhtin/Quest.java @@ -0,0 +1,208 @@ +package bakhtin; + +import bakhtin.Quest.Question.Answer; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import lombok.Getter; +import lombok.Setter; +import java.util.List; + +@Getter +public class Quest implements Serializable { + + @Setter + Question startQuestion; + String name; + ConcurrentHashMap questions; + + public Quest(String name) { + this.name = name; + questions = new ConcurrentHashMap<>(); + } + + public static QuestBuilder builder() { + return new QuestBuilder(); + } + + + public Question addNewQuestion(String question, List answers) { + Question q = new Question(question, answers); + if (q.getId().equals(1L)) { + startQuestion = q; + } + questions.put(q.getId(), q); + return q; + } + + public Question getQuestion(Long questionId) { + return questions.get(questionId); + } + + public static class QuestBuilder { + + private String name; + private Question startQuestion; + private final Map questions = new ConcurrentHashMap<>(); + + private QuestBuilder() { + } + + public QuestBuilder name(String name) { + this.name = name; + return this; + } + + public QuestBuilder question(Question question) { + if (question.getId() == 1L) { + startQuestion = question; + } + questions.put(question.getId(), question); + return this; + } + + public QuestBuilder startQuestion(Question startQuestion) { + this.startQuestion = startQuestion; + questions.put(startQuestion.getId(), startQuestion); + return this; + } + + public Quest build() { + Quest quest = new Quest(name); + quest.setStartQuestion(startQuestion); + quest.getQuestions().putAll(questions); + return quest; + } + + } + + + @Getter + public static class Question implements Serializable { + + private static final long serialVersionUID = 1L; + private boolean terminal; + private boolean win; + private static final AtomicLong idGenerator = new AtomicLong(0); + String question; + ConcurrentHashMap answers = new ConcurrentHashMap<>(); + Long id; + + public static QuestionBuilder builder() { + return new QuestionBuilder(); + } + + public Question(boolean win, String question) { + this.win = win; + this.question = question; + terminal = true; + id = idGenerator.incrementAndGet(); + } + + public Question(String question, List answers) { + this.question = question; + id = idGenerator.incrementAndGet(); + for (Answer answer : answers) { + this.answers.put(answer.id, answer); + } + terminal = false; + } + + public Answer getAnswer(Long answerId) { + return answers.get(answerId); + } + + public static class QuestionBuilder { + + private String question; + private List answers = new ArrayList<>(); + private boolean terminal; + private boolean win; + + private QuestionBuilder() { + } + + public QuestionBuilder question(String question) { + this.question = question; + return this; + } + + public QuestionBuilder answer(Answer answer) { + answers.add(answer); + return this; + } + + public QuestionBuilder win(boolean win) { + terminal = true; + this.win = win; + return this; + } + + public Question build() { + if (terminal) { + return new Question(win, question); + } else { + return new Question(question, answers); + } + } + + } + + @Getter + public static class Answer implements Serializable { + + private static final long serialVersionUID = 1L; + private static final AtomicLong idGenerator = new AtomicLong(0); + + private final Long id; + private final String text; + private @Setter + Question nextQuestion; + + + public static AnswerBuilder builder() { + return new AnswerBuilder(); + } + + public Answer(String text) { + this.text = text; + this.nextQuestion = null; + id = idGenerator.incrementAndGet(); + } + + public Answer(String text, Question nextQuestion) { + this.text = text; + this.nextQuestion = nextQuestion; + id = idGenerator.incrementAndGet(); + } + + public static class AnswerBuilder { + + private String answer; + private Question nextQuestion; + + public AnswerBuilder answer(String answer) { + this.answer = answer; + return this; + } + + public AnswerBuilder nextQuestion(Question nextQuestion) { + this.nextQuestion = nextQuestion; + return this; + } + + private AnswerBuilder() { + } + + public Answer build() { + return new Answer(answer, nextQuestion); + } + + } + + } + + } +} diff --git a/src/main/java/bakhtin/QuestServlet.java b/src/main/java/bakhtin/QuestServlet.java new file mode 100644 index 0000000..f1126b5 --- /dev/null +++ b/src/main/java/bakhtin/QuestServlet.java @@ -0,0 +1,189 @@ +package bakhtin; + +import bakhtin.Quest.Question; +import bakhtin.Quest.Question.Answer; +import bakhtin.exсeptions.IllegalActionException; +import bakhtin.exсeptions.NoActiveQuestException; +import bakhtin.exсeptions.NoAnswerGivenException; +import bakhtin.exсeptions.NoQuestionException; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +@WebServlet("/quest") +public class QuestServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String actionStr = req.getParameter("action"); + Actions action = getAction(actionStr); + + HttpSession session = req.getSession(); + + if (action == Actions.ANSWER) { + Object attr = session.getAttribute("currentQuestion"); + if (!(attr instanceof Question question)) { + throw new NoQuestionException("Question not found in session"); + } + String answerId = req.getParameter("answerId"); + Long id; + + if (answerId != null && !answerId.isEmpty() && answerId.matches("\\d+")) { + id = Long.parseLong(answerId); + } else { + throw new NoAnswerGivenException("Answer ID not found in session"); + } + + Question nextQuestion = getNextQuestion(question, id); + + if (nextQuestion.isTerminal()) { + if (nextQuestion.isWin()) { + req.setAttribute("win", "true"); + } else { + req.setAttribute("win", "false"); + } + + // Увеличиваем счетчик сыгранных игр + int gamesPlayed = (int) session.getAttribute("gamesPlayed"); + session.setAttribute("gamesPlayed", gamesPlayed + 1); + req.setAttribute("gamesPlayed", gamesPlayed + 1); + } + + req.setAttribute("currentQuestion", nextQuestion); + session.setAttribute("currentQuestion", nextQuestion); + req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); + + } else if (action == Actions.RESTART) { + restart(req, resp, session); + + } else if (action == Actions.EXIT) { + session.invalidate(); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } else { + throw new IllegalActionException("Invalid action: " + actionStr); + } + } + + protected Actions getAction(String actionStr) { + Actions action = null; + if (actionStr != null) { + try { + action = Actions.valueOf(actionStr.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalActionException("Action not found for: " + actionStr); + } + } + return action; + } + + protected Question getNextQuestion(Question question, Long answerId) { + Answer answer = question.getAnswer(answerId); + if (answer == null) { + throw new NoAnswerGivenException("Answer not found for ID: " + answerId); + } + + Question nextQuestion = answer.getNextQuestion(); + + if (nextQuestion == null) { + throw new NoQuestionException("No next question found for answer ID: " + answerId); + } + return nextQuestion; + } + + protected void restart(HttpServletRequest req, HttpServletResponse resp, HttpSession session) + throws ServletException, IOException { + session.removeAttribute("currentQuestion"); + session.removeAttribute("answerID"); + + Quest quest = (Quest) session.getAttribute("quest"); + + if (quest == null) { + throw new NoQuestionException("Quest not found in session"); + } + + Question startQuestion = quest.getStartQuestion(); + session.setAttribute("currentQuestion", startQuestion); + req.setAttribute("currentQuestion", startQuestion); + + req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (req.getSession().getAttribute("firstGetProcessed") == null) { + req.getSession().setAttribute("firstGetProcessed", true); + + // Инициализация счетчика игр + if (req.getSession().getAttribute("gamesPlayed") == null) { + req.getSession().setAttribute("gamesPlayed", 0); + } + + Quest newQuest = getStartQuest(); + + req.setAttribute("quest", newQuest); + + Question startQuestion = newQuest.getStartQuestion(); + HttpSession session = req.getSession(); + session.setAttribute("quest", newQuest); + session.setAttribute("currentQuestion", startQuestion); + } + + HttpSession session = req.getSession(); + Quest.Question currentQuestion = (Quest.Question) session.getAttribute("currentQuestion"); + + if (currentQuestion == null) { + Quest quest = (Quest) session.getAttribute("quest"); + if (quest == null) { + throw new NoActiveQuestException("Quest not found in session"); + } + currentQuestion = quest.getStartQuestion(); + } + + session.setAttribute("currentQuestion", currentQuestion); + + req.setAttribute("currentQuestion", currentQuestion); + req.setAttribute("gamesPlayed", session.getAttribute("gamesPlayed")); + req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); + } + + protected Quest getStartQuest() { + Quest newQuest = Quest.builder().name("base_quest").startQuestion( + Question.builder().question("Tы потерял память. Принять вызов НЛО?") + .answer(Answer.builder().answer("Принять вызов").nextQuestion( + Question.builder().question( + "Ты принял вызов. Поднимаешься на мостик к капитану?") + .answer(Answer.builder().answer("Подняться на мостик") + .nextQuestion(Question.builder() + .question("Ты поднялся на мостик. Ты кто?") + .answer(Answer.builder() + .answer("Рассказать правду о себе") + .nextQuestion(Question.builder() + .question( + "Тебя вернули домой. Победа!") + .win(true).build()).build()) + .answer(Answer.builder() + .answer("Солгать о себе") + .nextQuestion(Question.builder() + .question( + "Твою ложь разоблачили. Поражение.") + .win(false).build()) + .build()).build()).build()) + .answer(Answer.builder() + .answer("Отказаться подниматься на мостик") + .nextQuestion(Question.builder().question( + "Ты не пошел на переговоры. Поражение.") + .win(false).build()).build()).build()) + .build()).answer(Answer.builder().answer("Отклонить вызов") + .nextQuestion(Question.builder().question("Ты отклонил вызов. Поражение.") + .win(false).build()).build()).build()).build(); + return newQuest; + } +} diff --git "a/src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" "b/src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" new file mode 100644 index 0000000..53d8b6c --- /dev/null +++ "b/src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" @@ -0,0 +1,8 @@ +package bakhtin.exсeptions; + +public class IllegalActionException extends QuestException { + + public IllegalActionException(String message) { + super(message); + } +} diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" "b/src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" new file mode 100644 index 0000000..708da1d --- /dev/null +++ "b/src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" @@ -0,0 +1,8 @@ +package bakhtin.exсeptions; + +public class NoActiveQuestException extends QuestException { + + public NoActiveQuestException(String message) { + super(message); + } +} diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" "b/src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" new file mode 100644 index 0000000..6cfc4b0 --- /dev/null +++ "b/src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" @@ -0,0 +1,8 @@ +package bakhtin.exсeptions; + +public class NoAnswerGivenException extends QuestException { + + public NoAnswerGivenException(String message) { + super(message); + } +} diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" "b/src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" new file mode 100644 index 0000000..04fc18e --- /dev/null +++ "b/src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" @@ -0,0 +1,8 @@ +package bakhtin.exсeptions; + +public class NoQuestionException extends QuestException { + + public NoQuestionException(String message) { + super(message); + } +} diff --git "a/src/main/java/bakhtin/ex\321\201eptions/QuestException.java" "b/src/main/java/bakhtin/ex\321\201eptions/QuestException.java" new file mode 100644 index 0000000..e898369 --- /dev/null +++ "b/src/main/java/bakhtin/ex\321\201eptions/QuestException.java" @@ -0,0 +1,8 @@ +package bakhtin.exсeptions; + +public class QuestException extends RuntimeException { + + public QuestException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/controller/FrontController.java b/src/main/java/com/javarush/khmelov/controller/FrontController.java index 33242b2..e85c6a1 100644 --- a/src/main/java/com/javarush/khmelov/controller/FrontController.java +++ b/src/main/java/com/javarush/khmelov/controller/FrontController.java @@ -12,7 +12,7 @@ import java.io.IOException; -@WebServlet({"", "/home", "/list-user", "/edit-user"}) +@WebServlet({"/home", "/list-user", "/edit-user"}) public class FrontController extends HttpServlet { private final HttpResolver httpResolver = Winter.find(HttpResolver.class); diff --git a/src/main/webapp/WEB-INF/basics.jsp b/src/main/webapp/WEB-INF/basics.jsp deleted file mode 100644 index 80647b9..0000000 --- a/src/main/webapp/WEB-INF/basics.jsp +++ /dev/null @@ -1,157 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - - Основы сервлетов и JSP - - - -
-

Основы сервлетов и JSP

- - -
-
-

Основные концепции

-
-
-
    - -
  • ✓ ${concept}
  • -
    -
-
-
- - -
-
-

Структура сервлета

-
-
-
@WebServlet("/url")
-public class MyServlet extends HttpServlet {
-
-    @Override
-    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
-            throws ServletException, IOException {
-        // Обработка GET запроса
-
-        // 1. Подготовка данных
-        req.setAttribute("key", "value");
-
-        // 2. Forward на JSP
-        req.getRequestDispatcher("/WEB-INF/page.jsp")
-           .forward(req, resp);
-    }
-
-    @Override
-    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
-            throws ServletException, IOException {
-        // Обработка POST запроса (форма)
-
-        // 1. Получение данных из формы
-        String param = req.getParameter("paramName");
-
-        // 2. Обработка...
-
-        // 3. Redirect (изменяет URL в браузере)
-        resp.sendRedirect("/success");
-    }
-}
-
-
- - -
-
-

JSP - основные конструкции

-
-
-
1. JSP EL (Expression Language) - чтение данных
-
${attributeName}           // Атрибут request/session/application
-${param.paramName}        // Параметр запроса
-${requestScope.user.name} // Свойство объекта
-${1 + 2}                 // Выражения: 3
-${user.name != null}     // Условия
- -
2. JSTL - теги для логики
-
<%@ taglib uri="..." prefix="c" %>
-
-<c:forEach var="item" items="${list}">
-    ${item.name}
-</c:forEach>
-
-<c:if test="${user.admin}">
-    <a href="/admin">Админка</a>
-</c:if>
- -
3. Скриптлеты (устаревший способ)
-
<% String msg = "Привет"; %>
-<%= msg %>
-
- ⚠️ Не используйте скриптлеты! Используйте EL и JSTL вместо них. -
-
-
- - -
-
-

Forward vs Redirect

-
-
-
-
-
Forward
-
    -
  • Остаётся на том же URL
  • -
  • Данные request передаются
  • -
  • Быстрее (один запрос)
  • -
  • request.getRequestDispatcher().forward()
  • -
-
// URL: /hello
-// Контент: /WEB-INF/hello.jsp
-req.getRequestDispatcher("/WEB-INF/page.jsp")
-   .forward(req, resp);
-
-
-
Redirect
-
    -
  • URL меняется в браузере
  • -
  • Данные request теряются
  • -
  • Два запроса (два HTTP ответа)
  • -
  • response.sendRedirect()
  • -
-
// URL: /success
-// Контент: /WEB-INF/success.jsp
-resp.sendRedirect("/success");
-
-
-
-
- - -
-
-

Попробовать примеры

-
-
- -

- Время генерации страницы: ${currentTime} -

-
-
-
- - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/hello-result.jsp b/src/main/webapp/WEB-INF/hello-result.jsp deleted file mode 100644 index fdc4ffd..0000000 --- a/src/main/webapp/WEB-INF/hello-result.jsp +++ /dev/null @@ -1,27 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> - - - - Результат формы - - - -
-
-
-

Результат отправки формы

- -
-

Привет, ${submittedName}!

-

Это данные, которые пришли из формы через POST запрос.

-
- -
- - ← Вернуться назад -
-
-
- - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/hello.jsp b/src/main/webapp/WEB-INF/hello.jsp index 95b2efe..77ad960 100644 --- a/src/main/webapp/WEB-INF/hello.jsp +++ b/src/main/webapp/WEB-INF/hello.jsp @@ -1,50 +1,36 @@ <%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - Базовая страница - + Quest Pantera -
-
-
-

Базовая страница на сервлетах

+ + + + + + -
+
+
+
+

Добро пожаловать, пользователь!

- -

${message}

-

Добро пожаловать, ${userName}!

+
-
- - -

Тестовая форма

-
-
- - -
- -
- -
- -
- Как это работает: -
    -
  • Сервлет обрабатывает запрос по URL /hello
  • -
  • Сервлет готовит данные и кладет их в request.setAttribute()
  • -
  • Сервлет делает forward на JSP страницу
  • -
  • JSP использует ${attributeName} для чтения данных
  • -
-
+
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/quest.jsp b/src/main/webapp/WEB-INF/quest.jsp new file mode 100644 index 0000000..35f24be --- /dev/null +++ b/src/main/webapp/WEB-INF/quest.jsp @@ -0,0 +1,95 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + Quest Game + + + + +
+
+
+ + +
+
+
📊 Сыграно игр: ${gamesPlayed}
+
+
+ + +
+
+ + + +
+
+ 🎉 Победа! + 💀 Поражение +
+

${currentQuestion.question}

+
+
+ + + +

${currentQuestion.question}

+
+ + + +
+ + + + + +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ + + + +
+
+
+ + \ No newline at end of file diff --git a/src/test/java/bakhtin/ActionsTest.java b/src/test/java/bakhtin/ActionsTest.java new file mode 100644 index 0000000..7d29174 --- /dev/null +++ b/src/test/java/bakhtin/ActionsTest.java @@ -0,0 +1,22 @@ +package bakhtin; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ActionsTest { + + @Test + void testAllActionsAreAvailable() { + assertNotNull(Actions.ANSWER); + assertNotNull(Actions.RESTART); + assertNotNull(Actions.EXIT); + } + + @Test + void testNames() { + assertEquals("ANSWER", Actions.ANSWER.name()); + assertEquals("RESTART", Actions.RESTART.name()); + assertEquals("EXIT", Actions.EXIT.name()); + } +} \ No newline at end of file diff --git a/src/test/java/bakhtin/QuestServletTest.java b/src/test/java/bakhtin/QuestServletTest.java new file mode 100644 index 0000000..0c9c283 --- /dev/null +++ b/src/test/java/bakhtin/QuestServletTest.java @@ -0,0 +1,63 @@ +package bakhtin; + +import bakhtin.Quest.Question; +import bakhtin.Quest.Question.Answer; +import bakhtin.exсeptions.IllegalActionException; +import bakhtin.exсeptions.NoAnswerGivenException; +import bakhtin.exсeptions.NoQuestionException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class QuestServletTest { + + @Test + void getNextQuestion_validAnswer() { + Question nextQuestion = Question.builder().question("testQuestion").build(); + Answer answer = Answer.builder().answer("testAnswer").nextQuestion(nextQuestion).build(); + Question currentQuestion = Question.builder().question("currentTestQuestion").answer(answer) + .build(); + QuestServlet questServlet = new QuestServlet(); + Long id = answer.getId(); + Question expected = questServlet.getNextQuestion(currentQuestion, id); + Assertions.assertEquals(expected, nextQuestion); + } + + @Test + void getNextQuestion_validNoAnswerGivenException() { + Question currentQuestion = Question.builder().question("currentTestQuestion").build(); + QuestServlet questServlet = new QuestServlet(); + Long id = Long.MIN_VALUE; + Assertions.assertThrows(NoAnswerGivenException.class, () -> { + Question expected = questServlet.getNextQuestion(currentQuestion, id); + }); + } + + @Test + void getNextQuestion_validNoQuestionException() { + Answer answer = Answer.builder().answer("testAnswer").build(); + Question currentQuestion = Question.builder().question("currentTestQuestion").answer(answer) + .build(); + QuestServlet questServlet = new QuestServlet(); + Long id = answer.getId(); + Assertions.assertThrows(NoQuestionException.class, () -> { + questServlet.getNextQuestion(currentQuestion, id); + }); + } + + @Test + void getAction_validAnswer() { + QuestServlet questServlet = new QuestServlet(); + Question nextQuestion = Question.builder().question("testQuestion").build(); + var actual = questServlet.getAction("restart"); + Assertions.assertEquals(Actions.RESTART, actual); + } + + @Test + void getAction_illegalActionException() { + QuestServlet questServlet = new QuestServlet(); + + Assertions.assertThrows(IllegalActionException.class, () -> { + questServlet.getAction("***"); + }); + } +} \ No newline at end of file diff --git a/src/test/java/bakhtin/QuestTest.java b/src/test/java/bakhtin/QuestTest.java new file mode 100644 index 0000000..c5a509c --- /dev/null +++ b/src/test/java/bakhtin/QuestTest.java @@ -0,0 +1,36 @@ +package bakhtin; + +import static org.junit.jupiter.api.Assertions.*; + +import bakhtin.Quest.Question; +import org.junit.jupiter.api.Test; + +class QuestTest { + + @Test + void testBuilderName() { + Quest quest = Quest.builder().name("Test").build(); + assertEquals("Test", quest.getName()); + } + + @Test + void testBuilderQuestionWithAnswer() { + Question.Answer answer = Question.Answer.builder().answer("TestAnswer").build(); + Question question = Question.builder().question("TestQuestion").answer(answer).build(); + assertTrue(question.getAnswers().containsValue(answer)); + } + + @Test + void testQuestTerminalQuestion() { + Question question = Question.builder().question("TestQuestion").win(true).build(); + assertTrue(question.isTerminal()); + assertTrue(question.isWin()); + } + + @Test + void testQuestNextQuestion() { + Question question = Question.builder().question("TestQuestion").build(); + Question.Answer answer = Question.Answer.builder().answer("TestAnswer").nextQuestion(question).build(); + assertEquals(answer.getNextQuestion(), question); + } +} \ No newline at end of file From 9b008e08229d018a709061b393e8c0fa91416ec4 Mon Sep 17 00:00:00 2001 From: Denis Date: Tue, 10 Mar 2026 19:11:58 +0300 Subject: [PATCH 3/3] final --- pom.xml | 7 + src/main/java/bakhtin/Action.java | 18 ++ src/main/java/bakhtin/Actions.java | 7 - .../java/bakhtin/ErrorHandlingFilter.java | 18 +- src/main/java/bakhtin/FrontController.java | 4 - src/main/java/bakhtin/HelloServlet.java | 33 ---- src/main/java/bakhtin/Quest.java | 6 +- src/main/java/bakhtin/QuestServlet.java | 169 ++++++++---------- src/main/java/bakhtin/SessionConstants.java | 26 +++ .../exceptions/IllegalActionException.java | 2 +- .../exceptions/NoActiveQuestException.java | 2 +- .../exceptions/NoAnswerGivenException.java | 2 +- .../exceptions/NoQuestionException.java | 2 +- .../bakhtin/exceptions/QuestException.java | 2 +- src/main/webapp/WEB-INF/hello.jsp | 10 +- src/main/webapp/WEB-INF/quest.jsp | 30 ++-- src/main/webapp/WEB-INF/tags/constants.tag | 11 ++ src/test/java/bakhtin/ActionTest.java | 22 +++ src/test/java/bakhtin/ActionsTest.java | 22 --- src/test/java/bakhtin/QuestServletTest.java | 14 +- src/test/java/bakhtin/QuestTest.java | 2 +- 21 files changed, 210 insertions(+), 199 deletions(-) create mode 100644 src/main/java/bakhtin/Action.java delete mode 100644 src/main/java/bakhtin/Actions.java delete mode 100644 src/main/java/bakhtin/HelloServlet.java create mode 100644 src/main/java/bakhtin/SessionConstants.java rename "src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" => src/main/java/bakhtin/exceptions/IllegalActionException.java (83%) rename "src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" => src/main/java/bakhtin/exceptions/NoActiveQuestException.java (83%) rename "src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" => src/main/java/bakhtin/exceptions/NoAnswerGivenException.java (83%) rename "src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" => src/main/java/bakhtin/exceptions/NoQuestionException.java (82%) rename "src/main/java/bakhtin/ex\321\201eptions/QuestException.java" => src/main/java/bakhtin/exceptions/QuestException.java (81%) create mode 100644 src/main/webapp/WEB-INF/tags/constants.tag create mode 100644 src/test/java/bakhtin/ActionTest.java delete mode 100644 src/test/java/bakhtin/ActionsTest.java diff --git a/pom.xml b/pom.xml index 2aa0e92..cec3cce 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,13 @@ + + + org.apache.commons + commons-lang3 + 3.20.0 + compile + jakarta.servlet jakarta.servlet-api diff --git a/src/main/java/bakhtin/Action.java b/src/main/java/bakhtin/Action.java new file mode 100644 index 0000000..2e8257c --- /dev/null +++ b/src/main/java/bakhtin/Action.java @@ -0,0 +1,18 @@ +package bakhtin; + +import bakhtin.exceptions.IllegalActionException; + +public enum Action { + ANSWER, RESTART, EXIT; + + public static Action parse(String actionStr) { + if (actionStr != null) { + try { + return Action.valueOf(actionStr.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalActionException("Action not found for: " + actionStr); + } + } + throw new IllegalActionException("Action not found for: " + actionStr); + } +} \ No newline at end of file diff --git a/src/main/java/bakhtin/Actions.java b/src/main/java/bakhtin/Actions.java deleted file mode 100644 index 17a3cf9..0000000 --- a/src/main/java/bakhtin/Actions.java +++ /dev/null @@ -1,7 +0,0 @@ -package bakhtin; - -public enum Actions { - ANSWER, - RESTART, - EXIT -} diff --git a/src/main/java/bakhtin/ErrorHandlingFilter.java b/src/main/java/bakhtin/ErrorHandlingFilter.java index 34673f6..61fdb10 100644 --- a/src/main/java/bakhtin/ErrorHandlingFilter.java +++ b/src/main/java/bakhtin/ErrorHandlingFilter.java @@ -1,13 +1,15 @@ package bakhtin; -import bakhtin.exсeptions.IllegalActionException; -import bakhtin.exсeptions.NoActiveQuestException; -import bakhtin.exсeptions.NoAnswerGivenException; -import bakhtin.exсeptions.NoQuestionException; +import bakhtin.exceptions.IllegalActionException; +import bakhtin.exceptions.NoActiveQuestException; +import bakhtin.exceptions.NoAnswerGivenException; +import bakhtin.exceptions.NoQuestionException; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import java.io.IOException; +import static bakhtin.SessionConstants.*; + @WebFilter("/*") public class ErrorHandlingFilter implements Filter { @@ -18,19 +20,19 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain chain.doFilter(req, resp); } catch (NoActiveQuestException e) { // TODO(log): log.error("No active quest exception", e); - req.setAttribute("errorMessage", "Квест не найден. Начните квест заново."); + req.setAttribute(ERROR_MESSAGE_ATTR, "Квест не найден. Начните квест заново."); req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); } catch (NoAnswerGivenException e) { // TODO(log): log.error("No answer given exception", e); - req.setAttribute("errorMessage", "Не выбран ответ. Попробуйте ещё раз."); + req.setAttribute(ERROR_MESSAGE_ATTR, "Не выбран ответ. Попробуйте ещё раз."); req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); } catch (NoQuestionException e) { // TODO(log): log.error("No question exception", e); - req.setAttribute("errorMessage", "Вопрос не найден. Начните квест заново."); + req.setAttribute(ERROR_MESSAGE_ATTR, "Вопрос не найден. Начните квест заново."); req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); } catch (IllegalActionException e) { // TODO(log): log.error("Illegal action exception", e); - req.setAttribute("errorMessage", "Неверное действие. Попробуйте ещё раз"); + req.setAttribute(ERROR_MESSAGE_ATTR, "Неверное действие. Попробуйте ещё раз"); req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); } } diff --git a/src/main/java/bakhtin/FrontController.java b/src/main/java/bakhtin/FrontController.java index 3ed5c4e..d86d8a8 100644 --- a/src/main/java/bakhtin/FrontController.java +++ b/src/main/java/bakhtin/FrontController.java @@ -10,10 +10,6 @@ import java.util.ArrayList; import java.util.List; -/** - * Главный контроллер для папки bakhtin - * Перенаправляет на страницу с примерами - */ @WebServlet({"/bakhtin", "/bakhtin/", "/bakhtin/home", ""}) public class FrontController extends HttpServlet { diff --git a/src/main/java/bakhtin/HelloServlet.java b/src/main/java/bakhtin/HelloServlet.java deleted file mode 100644 index d07e943..0000000 --- a/src/main/java/bakhtin/HelloServlet.java +++ /dev/null @@ -1,33 +0,0 @@ -package bakhtin; - -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Простой сервлет, который обрабатывает GET запросы - * и передает управление на JSP страницу - */ -@WebServlet("/hello") -public class HelloServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // Подготовка данных для передачи в JSP - String message = "Привет из сервлета!"; - String userName = "Пользователь"; - - // Помещаем данные в request - так JSP сможет их прочитать - request.setAttribute("message", message); - request.setAttribute("userName", userName); - - // Forward - передаем управление на JSP страницу - // URL в браузере останется /hello, но контент будет от JSP - request.getRequestDispatcher("/WEB-INF/hello.jsp").forward(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/bakhtin/Quest.java b/src/main/java/bakhtin/Quest.java index 47bd05f..733528c 100644 --- a/src/main/java/bakhtin/Quest.java +++ b/src/main/java/bakhtin/Quest.java @@ -83,7 +83,7 @@ public Quest build() { public static class Question implements Serializable { private static final long serialVersionUID = 1L; - private boolean terminal; + private boolean finish; private boolean win; private static final AtomicLong idGenerator = new AtomicLong(0); String question; @@ -97,7 +97,7 @@ public static QuestionBuilder builder() { public Question(boolean win, String question) { this.win = win; this.question = question; - terminal = true; + finish = true; id = idGenerator.incrementAndGet(); } @@ -107,7 +107,7 @@ public Question(String question, List answers) { for (Answer answer : answers) { this.answers.put(answer.id, answer); } - terminal = false; + finish = false; } public Answer getAnswer(Long answerId) { diff --git a/src/main/java/bakhtin/QuestServlet.java b/src/main/java/bakhtin/QuestServlet.java index f1126b5..583b994 100644 --- a/src/main/java/bakhtin/QuestServlet.java +++ b/src/main/java/bakhtin/QuestServlet.java @@ -2,10 +2,10 @@ import bakhtin.Quest.Question; import bakhtin.Quest.Question.Answer; -import bakhtin.exсeptions.IllegalActionException; -import bakhtin.exсeptions.NoActiveQuestException; -import bakhtin.exсeptions.NoAnswerGivenException; -import bakhtin.exсeptions.NoQuestionException; +import bakhtin.exceptions.IllegalActionException; +import bakhtin.exceptions.NoActiveQuestException; +import bakhtin.exceptions.NoAnswerGivenException; +import bakhtin.exceptions.NoQuestionException; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; @@ -13,6 +13,9 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; +import org.apache.commons.lang3.StringUtils; + +import static bakhtin.SessionConstants.*; @WebServlet("/quest") public class QuestServlet extends HttpServlet { @@ -21,67 +24,64 @@ public class QuestServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String actionStr = req.getParameter("action"); - Actions action = getAction(actionStr); + String actionStr = req.getParameter(SessionConstants.ACTION_PARAM); + Action action = Action.parse(actionStr); HttpSession session = req.getSession(); - if (action == Actions.ANSWER) { - Object attr = session.getAttribute("currentQuestion"); - if (!(attr instanceof Question question)) { - throw new NoQuestionException("Question not found in session"); - } - String answerId = req.getParameter("answerId"); - Long id; - - if (answerId != null && !answerId.isEmpty() && answerId.matches("\\d+")) { - id = Long.parseLong(answerId); - } else { - throw new NoAnswerGivenException("Answer ID not found in session"); - } - - Question nextQuestion = getNextQuestion(question, id); - - if (nextQuestion.isTerminal()) { - if (nextQuestion.isWin()) { - req.setAttribute("win", "true"); - } else { - req.setAttribute("win", "false"); - } - - // Увеличиваем счетчик сыгранных игр - int gamesPlayed = (int) session.getAttribute("gamesPlayed"); - session.setAttribute("gamesPlayed", gamesPlayed + 1); - req.setAttribute("gamesPlayed", gamesPlayed + 1); - } - - req.setAttribute("currentQuestion", nextQuestion); - session.setAttribute("currentQuestion", nextQuestion); - req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); - - } else if (action == Actions.RESTART) { - restart(req, resp, session); - - } else if (action == Actions.EXIT) { - session.invalidate(); - req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + switch (action) { + case ANSWER -> doAnswer(req, resp, session); + case RESTART -> doRestart(req, resp, session); + case EXIT -> doExit(req, resp, session); + default -> throw new IllegalActionException("Invalid action: " + actionStr); + } + } + + private void doExit(HttpServletRequest req, HttpServletResponse resp, + HttpSession session) throws ServletException, IOException { + session.invalidate(); + req.getRequestDispatcher("/WEB-INF/hello.jsp").forward(req, resp); + } + + private void doAnswer(HttpServletRequest req, HttpServletResponse resp, HttpSession session) + throws ServletException, IOException { + Object attr = session.getAttribute(CURRENT_QUESTION_ATTR); + if (!(attr instanceof Question question)) { + throw new NoQuestionException("Question not found in session"); + } + String answerId = req.getParameter(ANSWER_ID_PARAM); + long id; + if (StringUtils.isNotEmpty(answerId)) { + id = Long.parseLong(answerId); } else { - throw new IllegalActionException("Invalid action: " + actionStr); + throw new NoAnswerGivenException("Answer ID not found in session"); + } + + Question nextQuestion = getNextQuestion(question, id); + + if (nextQuestion.isFinish()) { + doFinish(req, session, nextQuestion); } + + session.setAttribute(CURRENT_QUESTION_ATTR, nextQuestion); + req.setAttribute(CURRENT_QUESTION_REQUEST_ATTR, nextQuestion); + req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); } - protected Actions getAction(String actionStr) { - Actions action = null; - if (actionStr != null) { - try { - action = Actions.valueOf(actionStr.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new IllegalActionException("Action not found for: " + actionStr); - } + private void doFinish(HttpServletRequest req, HttpSession session, + Question nextQuestion) { + if (nextQuestion.isWin()) { + req.setAttribute(WIN_ATTR, "true"); + } else { + req.setAttribute(WIN_ATTR, "false"); } - return action; + + // Увеличиваем счетчик сыгранных игр + int gamesPlayed = (int) session.getAttribute(GAMES_PLAYED_ATTR); + session.setAttribute(GAMES_PLAYED_ATTR, gamesPlayed + 1); } + protected Question getNextQuestion(Question question, Long answerId) { Answer answer = question.getAnswer(answerId); if (answer == null) { @@ -96,20 +96,22 @@ protected Question getNextQuestion(Question question, Long answerId) { return nextQuestion; } - protected void restart(HttpServletRequest req, HttpServletResponse resp, HttpSession session) + protected void doRestart(HttpServletRequest req, HttpServletResponse resp, HttpSession session) throws ServletException, IOException { - session.removeAttribute("currentQuestion"); - session.removeAttribute("answerID"); + req.removeAttribute(CURRENT_QUESTION_REQUEST_ATTR); + req.removeAttribute(ANSWER_ID_PARAM); - Quest quest = (Quest) session.getAttribute("quest"); + Object objQuest = session.getAttribute(QUEST_ATTR); - if (quest == null) { + if (objQuest == null) { throw new NoQuestionException("Quest not found in session"); } + Quest quest = (Quest) objQuest; + Question startQuestion = quest.getStartQuestion(); - session.setAttribute("currentQuestion", startQuestion); - req.setAttribute("currentQuestion", startQuestion); + session.setAttribute(CURRENT_QUESTION_ATTR, startQuestion); + req.setAttribute(CURRENT_QUESTION_REQUEST_ATTR, startQuestion); req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); } @@ -117,41 +119,26 @@ protected void restart(HttpServletRequest req, HttpServletResponse resp, HttpSes @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HttpSession session = req.getSession(); - if (req.getSession().getAttribute("firstGetProcessed") == null) { - req.getSession().setAttribute("firstGetProcessed", true); - - // Инициализация счетчика игр - if (req.getSession().getAttribute("gamesPlayed") == null) { - req.getSession().setAttribute("gamesPlayed", 0); - } - - Quest newQuest = getStartQuest(); - - req.setAttribute("quest", newQuest); - - Question startQuestion = newQuest.getStartQuestion(); - HttpSession session = req.getSession(); - session.setAttribute("quest", newQuest); - session.setAttribute("currentQuestion", startQuestion); + if (session.getAttribute(FIRST_GET_PROCESSED_ATTR) == null) { + prepareSession(session, req); } + req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); + } - HttpSession session = req.getSession(); - Quest.Question currentQuestion = (Quest.Question) session.getAttribute("currentQuestion"); - - if (currentQuestion == null) { - Quest quest = (Quest) session.getAttribute("quest"); - if (quest == null) { - throw new NoActiveQuestException("Quest not found in session"); - } - currentQuestion = quest.getStartQuestion(); + private void prepareSession(HttpSession session, HttpServletRequest request) { + session.setAttribute(FIRST_GET_PROCESSED_ATTR, true); + + if (session.getAttribute(GAMES_PLAYED_ATTR) == null) { + session.setAttribute(GAMES_PLAYED_ATTR, 0); } - session.setAttribute("currentQuestion", currentQuestion); + Quest newQuest = getStartQuest(); - req.setAttribute("currentQuestion", currentQuestion); - req.setAttribute("gamesPlayed", session.getAttribute("gamesPlayed")); - req.getRequestDispatcher("/WEB-INF/quest.jsp").forward(req, resp); + session.setAttribute(QUEST_ATTR, newQuest); + session.setAttribute(CURRENT_QUESTION_ATTR, newQuest.getStartQuestion()); + request.setAttribute(CURRENT_QUESTION_REQUEST_ATTR, newQuest.getStartQuestion()); } protected Quest getStartQuest() { diff --git a/src/main/java/bakhtin/SessionConstants.java b/src/main/java/bakhtin/SessionConstants.java new file mode 100644 index 0000000..bb9e163 --- /dev/null +++ b/src/main/java/bakhtin/SessionConstants.java @@ -0,0 +1,26 @@ +package bakhtin; + +/** + * Константы для атрибутов сессии, запроса и параметров запроса + */ +public final class SessionConstants { + + // Параметры запроса + public static final String ACTION_PARAM = "action"; + public static final String ANSWER_ID_PARAM = "answerId"; + + // Атрибуты сессии + public static final String QUEST_ATTR = "quest"; + public static final String CURRENT_QUESTION_ATTR = "currentQuestion"; + public static final String FIRST_GET_PROCESSED_ATTR = "firstGetProcessed"; + public static final String GAMES_PLAYED_ATTR = "gamesPlayed"; + + // Атрибуты запроса + public static final String CURRENT_QUESTION_REQUEST_ATTR = "currentQuestion"; + public static final String WIN_ATTR = "win"; + public static final String ERROR_MESSAGE_ATTR = "errorMessage"; + + private SessionConstants() { + // Приватный конструктор для предотвращения создания экземпляров + } +} \ No newline at end of file diff --git "a/src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" b/src/main/java/bakhtin/exceptions/IllegalActionException.java similarity index 83% rename from "src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" rename to src/main/java/bakhtin/exceptions/IllegalActionException.java index 53d8b6c..98eaed4 100644 --- "a/src/main/java/bakhtin/ex\321\201eptions/IllegalActionException.java" +++ b/src/main/java/bakhtin/exceptions/IllegalActionException.java @@ -1,4 +1,4 @@ -package bakhtin.exсeptions; +package bakhtin.exceptions; public class IllegalActionException extends QuestException { diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" b/src/main/java/bakhtin/exceptions/NoActiveQuestException.java similarity index 83% rename from "src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" rename to src/main/java/bakhtin/exceptions/NoActiveQuestException.java index 708da1d..9275e91 100644 --- "a/src/main/java/bakhtin/ex\321\201eptions/NoActiveQuestException.java" +++ b/src/main/java/bakhtin/exceptions/NoActiveQuestException.java @@ -1,4 +1,4 @@ -package bakhtin.exсeptions; +package bakhtin.exceptions; public class NoActiveQuestException extends QuestException { diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" b/src/main/java/bakhtin/exceptions/NoAnswerGivenException.java similarity index 83% rename from "src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" rename to src/main/java/bakhtin/exceptions/NoAnswerGivenException.java index 6cfc4b0..214c10a 100644 --- "a/src/main/java/bakhtin/ex\321\201eptions/NoAnswerGivenException.java" +++ b/src/main/java/bakhtin/exceptions/NoAnswerGivenException.java @@ -1,4 +1,4 @@ -package bakhtin.exсeptions; +package bakhtin.exceptions; public class NoAnswerGivenException extends QuestException { diff --git "a/src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" b/src/main/java/bakhtin/exceptions/NoQuestionException.java similarity index 82% rename from "src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" rename to src/main/java/bakhtin/exceptions/NoQuestionException.java index 04fc18e..06bb1db 100644 --- "a/src/main/java/bakhtin/ex\321\201eptions/NoQuestionException.java" +++ b/src/main/java/bakhtin/exceptions/NoQuestionException.java @@ -1,4 +1,4 @@ -package bakhtin.exсeptions; +package bakhtin.exceptions; public class NoQuestionException extends QuestException { diff --git "a/src/main/java/bakhtin/ex\321\201eptions/QuestException.java" b/src/main/java/bakhtin/exceptions/QuestException.java similarity index 81% rename from "src/main/java/bakhtin/ex\321\201eptions/QuestException.java" rename to src/main/java/bakhtin/exceptions/QuestException.java index e898369..ba69cbd 100644 --- "a/src/main/java/bakhtin/ex\321\201eptions/QuestException.java" +++ b/src/main/java/bakhtin/exceptions/QuestException.java @@ -1,4 +1,4 @@ -package bakhtin.exсeptions; +package bakhtin.exceptions; public class QuestException extends RuntimeException { diff --git a/src/main/webapp/WEB-INF/hello.jsp b/src/main/webapp/WEB-INF/hello.jsp index 77ad960..6cee019 100644 --- a/src/main/webapp/WEB-INF/hello.jsp +++ b/src/main/webapp/WEB-INF/hello.jsp @@ -1,5 +1,6 @@ <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib tagdir="/WEB-INF/tags" prefix="my" %> @@ -8,14 +9,15 @@ rel="stylesheet"> - + + - + diff --git a/src/main/webapp/WEB-INF/quest.jsp b/src/main/webapp/WEB-INF/quest.jsp index 35f24be..c2a2ba8 100644 --- a/src/main/webapp/WEB-INF/quest.jsp +++ b/src/main/webapp/WEB-INF/quest.jsp @@ -1,5 +1,6 @@ <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib tagdir="/WEB-INF/tags" prefix="my" %> @@ -17,6 +18,7 @@ +
@@ -24,7 +26,7 @@
-
📊 Сыграно игр: ${gamesPlayed}
+
📊 Сыграно игр: ${requestScope[GAMES_PLAYED_ATTR]}
@@ -33,29 +35,29 @@
- -
+ +
- 🎉 Победа! - 💀 Поражение + 🎉 Победа! + 💀 Поражение
-

${currentQuestion.question}

+

${requestScope[CURRENT_QUESTION_ATTR].question}

- -

${currentQuestion.question}

+ +

${requestScope[CURRENT_QUESTION_ATTR].question}

- +
- + - +
- +
diff --git a/src/main/webapp/WEB-INF/tags/constants.tag b/src/main/webapp/WEB-INF/tags/constants.tag new file mode 100644 index 0000000..e417d80 --- /dev/null +++ b/src/main/webapp/WEB-INF/tags/constants.tag @@ -0,0 +1,11 @@ +<%@ tag description="Provides access to session constants" pageEncoding="UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/bakhtin/ActionTest.java b/src/test/java/bakhtin/ActionTest.java new file mode 100644 index 0000000..9ba616b --- /dev/null +++ b/src/test/java/bakhtin/ActionTest.java @@ -0,0 +1,22 @@ +package bakhtin; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ActionTest { + + @Test + void testAllActionsAreAvailable() { + assertNotNull(Action.ANSWER); + assertNotNull(Action.RESTART); + assertNotNull(Action.EXIT); + } + + @Test + void testNames() { + assertEquals("ANSWER", Action.ANSWER.name()); + assertEquals("RESTART", Action.RESTART.name()); + assertEquals("EXIT", Action.EXIT.name()); + } +} \ No newline at end of file diff --git a/src/test/java/bakhtin/ActionsTest.java b/src/test/java/bakhtin/ActionsTest.java deleted file mode 100644 index 7d29174..0000000 --- a/src/test/java/bakhtin/ActionsTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package bakhtin; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -class ActionsTest { - - @Test - void testAllActionsAreAvailable() { - assertNotNull(Actions.ANSWER); - assertNotNull(Actions.RESTART); - assertNotNull(Actions.EXIT); - } - - @Test - void testNames() { - assertEquals("ANSWER", Actions.ANSWER.name()); - assertEquals("RESTART", Actions.RESTART.name()); - assertEquals("EXIT", Actions.EXIT.name()); - } -} \ No newline at end of file diff --git a/src/test/java/bakhtin/QuestServletTest.java b/src/test/java/bakhtin/QuestServletTest.java index 0c9c283..d201993 100644 --- a/src/test/java/bakhtin/QuestServletTest.java +++ b/src/test/java/bakhtin/QuestServletTest.java @@ -2,9 +2,9 @@ import bakhtin.Quest.Question; import bakhtin.Quest.Question.Answer; -import bakhtin.exсeptions.IllegalActionException; -import bakhtin.exсeptions.NoAnswerGivenException; -import bakhtin.exсeptions.NoQuestionException; +import bakhtin.exceptions.IllegalActionException; +import bakhtin.exceptions.NoAnswerGivenException; +import bakhtin.exceptions.NoQuestionException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ void getNextQuestion_validAnswer() { void getNextQuestion_validNoAnswerGivenException() { Question currentQuestion = Question.builder().question("currentTestQuestion").build(); QuestServlet questServlet = new QuestServlet(); - Long id = Long.MIN_VALUE; + Long id = -1L; Assertions.assertThrows(NoAnswerGivenException.class, () -> { Question expected = questServlet.getNextQuestion(currentQuestion, id); }); @@ -48,8 +48,8 @@ void getNextQuestion_validNoQuestionException() { void getAction_validAnswer() { QuestServlet questServlet = new QuestServlet(); Question nextQuestion = Question.builder().question("testQuestion").build(); - var actual = questServlet.getAction("restart"); - Assertions.assertEquals(Actions.RESTART, actual); + var actual = Action.parse("restart"); + Assertions.assertEquals(Action.RESTART, actual); } @Test @@ -57,7 +57,7 @@ void getAction_illegalActionException() { QuestServlet questServlet = new QuestServlet(); Assertions.assertThrows(IllegalActionException.class, () -> { - questServlet.getAction("***"); + Action.parse("***"); }); } } \ No newline at end of file diff --git a/src/test/java/bakhtin/QuestTest.java b/src/test/java/bakhtin/QuestTest.java index c5a509c..2d250b9 100644 --- a/src/test/java/bakhtin/QuestTest.java +++ b/src/test/java/bakhtin/QuestTest.java @@ -23,7 +23,7 @@ void testBuilderQuestionWithAnswer() { @Test void testQuestTerminalQuestion() { Question question = Question.builder().question("TestQuestion").win(true).build(); - assertTrue(question.isTerminal()); + assertTrue(question.isFinish()); assertTrue(question.isWin()); }