diff --git a/.gitignore b/.gitignore index a2afc5d..c38ea4f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,11 @@ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ +*.class ### IntelliJ IDEA ### .idea - +*.iml ### Eclipse ### .apt_generated diff --git a/pom.xml b/pom.xml index 78ee59d..d192786 100644 --- a/pom.xml +++ b/pom.xml @@ -1,94 +1,114 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.javarush.khmelov - project-ledzeppelin + com.javarush.ushanov + project-pantera 1.0-SNAPSHOT - ProjectLedzeppelin + war - UTF-8 - 21 21 - 5.10.2 + 21 + UTF-8 - - org.springframework.boot - spring-boot-starter-parent - 3.3.5 - - - - - - org.springframework.boot - spring-boot-dependencies - pom - import - 3.3.5 - - - - + jakarta.servlet jakarta.servlet-api + 6.0.0 provided + + jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api + 3.0.0 org.glassfish.web jakarta.servlet.jsp.jstl + 3.0.1 + org.projectlombok lombok + 1.18.30 provided + + + + org.slf4j + slf4j-api + 2.0.9 + + + - org.junit.jupiter - junit-jupiter-api - test + ch.qos.logback + logback-classic + 1.5.32 + + org.junit.jupiter - junit-jupiter-engine + junit-jupiter + 5.10.1 test - + org.apache.maven.plugins maven-war-plugin 3.4.0 + org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - 1.18.34 - - - + maven-surefire-plugin + 3.1.2 - \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/Command.java b/src/main/java/com/javarush/khmelov/cmd/Command.java deleted file mode 100644 index fd4035b..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/Command.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.javarush.khmelov.cmd; - -import jakarta.servlet.http.HttpServletRequest; - -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public interface Command { - - default String doGet(HttpServletRequest request) { - return getView(); - } - - default String doPost(HttpServletRequest request) { - return getView(); - } - - default String getView() { - String simpleName = this.getClass().getSimpleName(); - return convertCamelCaseToKebabStyle(simpleName); - } - - private static String convertCamelCaseToKebabStyle(String string) { - String snakeName = string.chars() - .mapToObj(s -> String.valueOf((char) s)) - .flatMap(s -> s.matches("[A-Z]") - ? Stream.of("-", s) - : Stream.of(s)) - .collect(Collectors.joining()) - .toLowerCase(); - return snakeName.startsWith("-") - ? snakeName.substring(1) - : snakeName; - } - - -} diff --git a/src/main/java/com/javarush/khmelov/cmd/EditUser.java b/src/main/java/com/javarush/khmelov/cmd/EditUser.java deleted file mode 100644 index ae191b4..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/EditUser.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.javarush.khmelov.cmd; - -import com.javarush.khmelov.entity.Role; -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.service.UserService; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Optional; - - -@SuppressWarnings("unused") -public class EditUser implements Command { - - private final UserService userService; - - public EditUser(UserService userService) { - this.userService = userService; - } - - - @Override - public String doGet(HttpServletRequest req) { - String stringId = req.getParameter("id"); - if (stringId != null) { - long id = Long.parseLong(stringId); - Optional optionalUser = userService.get(id); - if (optionalUser.isPresent()) { - User user = optionalUser.get(); - req.setAttribute("user", user); - } - } - return getView(); - } - - @Override - public String doPost(HttpServletRequest req) { - User user = User.builder() - .login(req.getParameter("login")) - .password(req.getParameter("password")) - .role(Role.valueOf(req.getParameter("role"))) - .build(); - if (req.getParameter("create") != null) { - userService.create(user); - } else if (req.getParameter("update") != null) { - user.setId(Long.parseLong(req.getParameter("id"))); - userService.update(user); - } - return getView() + "?id=" + user.getId(); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/ListUser.java b/src/main/java/com/javarush/khmelov/cmd/ListUser.java deleted file mode 100644 index 9257917..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/ListUser.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.cmd; - -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.service.UserService; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Collection; - -@SuppressWarnings("unused") -public class ListUser implements Command { - - private final UserService userService; - - public ListUser(UserService userService) { - this.userService = userService; - } - - @Override - public String doGet(HttpServletRequest request) { - Collection users = userService.getAll(); - request.setAttribute("users", users); - return getView(); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/StartPage.java b/src/main/java/com/javarush/khmelov/cmd/StartPage.java deleted file mode 100644 index d268f93..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/StartPage.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.javarush.khmelov.cmd; - -@SuppressWarnings("unused") -public class StartPage implements Command { - -} diff --git a/src/main/java/com/javarush/khmelov/config/Winter.java b/src/main/java/com/javarush/khmelov/config/Winter.java deleted file mode 100644 index 48bd8a7..0000000 --- a/src/main/java/com/javarush/khmelov/config/Winter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.javarush.khmelov.config; - -import lombok.SneakyThrows; - -import java.lang.reflect.Constructor; -import java.util.concurrent.ConcurrentHashMap; - -public class Winter { - - public static ConcurrentHashMap, Object> components = new ConcurrentHashMap<>(); - - - @SuppressWarnings("unchecked") - @SneakyThrows - public static T find(Class aClass) { - Object component = components.get(aClass); - if (component == null) { - Constructor> constructor = aClass.getConstructors()[0]; - Class>[] parameterTypes = constructor.getParameterTypes(); - Object[] parameters = new Object[parameterTypes.length]; - for (int i = 0; i < parameters.length; i++) { - parameters[i] = Winter.find(parameterTypes[i]); - } - Object newInstance = constructor.newInstance(parameters); - components.put(aClass, newInstance); - } - return (T) components.get(aClass); - } -} diff --git a/src/main/java/com/javarush/khmelov/controller/FrontController.java b/src/main/java/com/javarush/khmelov/controller/FrontController.java deleted file mode 100644 index 33242b2..0000000 --- a/src/main/java/com/javarush/khmelov/controller/FrontController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.javarush.khmelov.controller; - -import com.javarush.khmelov.cmd.Command; -import com.javarush.khmelov.config.Winter; -import com.javarush.khmelov.entity.Role; -import jakarta.servlet.ServletConfig; -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; - -@WebServlet({"", "/home", "/list-user", "/edit-user"}) -public class FrontController extends HttpServlet { - - private final HttpResolver httpResolver = Winter.find(HttpResolver.class); - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Command command = httpResolver.resolve(req); - String view = command.doGet(req); - String jsp = getJsp(view); - req.getRequestDispatcher(jsp).forward(req, resp); - } - - @Override - public void init(ServletConfig config) { - config.getServletContext().setAttribute("roles", Role.values()); - } - - private static String getJsp(String view) { - return "/WEB-INF/" + view + ".jsp"; - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Command command = httpResolver.resolve(req); - String redirect = command.doPost(req); - resp.sendRedirect(redirect); - } -} diff --git a/src/main/java/com/javarush/khmelov/controller/HttpResolver.java b/src/main/java/com/javarush/khmelov/controller/HttpResolver.java deleted file mode 100644 index 18bb761..0000000 --- a/src/main/java/com/javarush/khmelov/controller/HttpResolver.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.javarush.khmelov.controller; - -import com.javarush.khmelov.cmd.Command; -import com.javarush.khmelov.config.Winter; -import jakarta.servlet.http.HttpServletRequest; - -public class HttpResolver { - - public Command resolve(HttpServletRequest request) { - // /cmd-example - try { - String requestURI = request.getRequestURI(); - requestURI = requestURI.equals("/") ? "/start-page" : requestURI; - String kebabName = requestURI.split("[?#/]")[1]; - String simpleName = convertKebabStyleToCamelCase(kebabName); - String fullName = Command.class.getPackageName() + "." + simpleName; - Class> aClass = Class.forName(fullName); - return (Command) Winter.find(aClass); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - private String convertKebabStyleToCamelCase(String input) { - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = true; - for (char c : input.toCharArray()) { - if (c == '-') { - capitalizeNext = true; - } else { - if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(Character.toLowerCase(c)); - } - } - } - return result.toString(); - } -} diff --git a/src/main/java/com/javarush/khmelov/entity/Role.java b/src/main/java/com/javarush/khmelov/entity/Role.java deleted file mode 100644 index 5ae365f..0000000 --- a/src/main/java/com/javarush/khmelov/entity/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.entity; - -public enum Role { - USER, ADMIN, GUEST -} diff --git a/src/main/java/com/javarush/khmelov/entity/User.java b/src/main/java/com/javarush/khmelov/entity/User.java deleted file mode 100644 index f7fa2d6..0000000 --- a/src/main/java/com/javarush/khmelov/entity/User.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.entity; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class User { - - private Long id; - - private String login; - - private String password; - - private Role role; - - public String getImage() { //TODO move to DTO - return "image-" + id; - } - -} diff --git a/src/main/java/com/javarush/khmelov/repository/Repository.java b/src/main/java/com/javarush/khmelov/repository/Repository.java deleted file mode 100644 index f1abdac..0000000 --- a/src/main/java/com/javarush/khmelov/repository/Repository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.javarush.khmelov.repository; - -import com.javarush.khmelov.entity.User; - -import java.util.Collection; -import java.util.Optional; - -public interface Repository { - - Collection getAll(); - - Optional get(long id); - - void create(T entity); - - void update(T entity); - - void delete(T entity); -} diff --git a/src/main/java/com/javarush/khmelov/repository/UserRepository.java b/src/main/java/com/javarush/khmelov/repository/UserRepository.java deleted file mode 100644 index 58b32ea..0000000 --- a/src/main/java/com/javarush/khmelov/repository/UserRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.javarush.khmelov.repository; - -import com.javarush.khmelov.entity.Role; -import com.javarush.khmelov.entity.User; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; - -public class UserRepository implements Repository { - - private final Map map = new HashMap<>(); - - public static final AtomicLong id = new AtomicLong(System.currentTimeMillis()); - - public UserRepository() { - map.put(1L, new User(1L, "Alisa", "qwerty", Role.USER)); - map.put(2L, new User(2L, "Bob", "", Role.GUEST)); - map.put(3L, new User(3L, "Carl", "admin", Role.ADMIN)); - map.put(4L, new User(4L, "Khmelov", "admin", Role.ADMIN)); - } - - @Override - public Collection getAll() { - return map.values(); - } - - @Override - public Optional get(long id) { - return Optional.ofNullable(map.get(id)); - } - - @Override - public void create(User entity) { - entity.setId(id.incrementAndGet()); - update(entity); - } - - @Override - public void update(User entity) { - map.put(entity.getId(), entity); - } - - @Override - public void delete(User entity) { - map.remove(entity.getId()); - } -} diff --git a/src/main/java/com/javarush/khmelov/service/UserService.java b/src/main/java/com/javarush/khmelov/service/UserService.java deleted file mode 100644 index b17527c..0000000 --- a/src/main/java/com/javarush/khmelov/service/UserService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.javarush.khmelov.service; - -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.repository.UserRepository; - -import java.util.Collection; -import java.util.Optional; - -public class UserService { - - private final UserRepository userRepository; - - public UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public void create(User user) { - userRepository.create(user); - } - - public void update(User user) { - userRepository.update(user); - } - - public void delete(User user) { - userRepository.delete(user); - } - - public Collection getAll() { - return userRepository.getAll(); - } - - public Optional get(long id) { - return userRepository.get(id); - } -} diff --git a/src/main/java/com/javarush/ushanov/cmd/ChoiceCommand.java b/src/main/java/com/javarush/ushanov/cmd/ChoiceCommand.java new file mode 100644 index 0000000..ef351e6 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/cmd/ChoiceCommand.java @@ -0,0 +1,47 @@ +package com.javarush.ushanov.cmd; + +import com.javarush.ushanov.entity.GameSession; +import com.javarush.ushanov.service.SessionService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Команда для обработки выбора игрока. + * + * POST /choice — игрок нажал на кнопку с вариантом ответа. + * + * Логика: + * 1. Читаем параметр "option" из POST-запроса (текст кнопки) + * 2. Через SessionService обновляем текущий шаг в сессии + * 3. Делаем redirect на GET /quest — паттерн PRG (Post-Redirect-Get) + * + * Паттерн PRG важен: если после POST-запроса не делать redirect, + * и пользователь обновит страницу (F5) — браузер отправит POST повторно, + * и выбор будет сделан ещё раз. Redirect это предотвращает. + */ +public class ChoiceCommand implements Command { + + private final SessionService sessionService; + + public ChoiceCommand(SessionService sessionService) { + this.sessionService = sessionService; + } + + @Override + public String execute(HttpServletRequest request, HttpServletResponse response) { + GameSession gameSession = sessionService.getSession(request.getSession()); + + if (gameSession == null) { + return "redirect:/"; + } + + String chosenOption = request.getParameter("option"); + + if (chosenOption != null && !chosenOption.isEmpty()) { + sessionService.processChoice(request.getSession(), chosenOption); + } + + // PRG паттерн: после обработки POST — redirect на GET + return "redirect:/quest"; + } +} diff --git a/src/main/java/com/javarush/ushanov/cmd/Command.java b/src/main/java/com/javarush/ushanov/cmd/Command.java new file mode 100644 index 0000000..751a179 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/cmd/Command.java @@ -0,0 +1,25 @@ +package com.javarush.ushanov.cmd; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Интерфейс команды — основа паттерна Command. + * + * Паттерн Command — каждое действие (показать страницу, обработать выбор) + * оформляется как отдельный класс, реализующий этот интерфейс. + * + * Метод execute() выполняет действие и возвращает путь к JSP-странице, + * которую нужно показать пользователю (или путь для redirect). + */ +public interface Command { + + /** + * Выполнить команду. + * + * @param request HTTP-запрос (содержит параметры, сессию и т.д.) + * @param response HTTP-ответ + * @return путь к JSP-странице для отображения, или "redirect:/path" для перенаправления + */ + String execute(HttpServletRequest request, HttpServletResponse response); +} diff --git a/src/main/java/com/javarush/ushanov/cmd/NewGameCommand.java b/src/main/java/com/javarush/ushanov/cmd/NewGameCommand.java new file mode 100644 index 0000000..aa77d20 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/cmd/NewGameCommand.java @@ -0,0 +1,43 @@ +package com.javarush.ushanov.cmd; + +import com.javarush.ushanov.entity.GameSession; +import com.javarush.ushanov.service.SessionService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Команда для начала новой игры. + * + * Обрабатывает два случая: + * 1. Первый запуск: игрок ввёл имя на стартовой странице — создаём новую сессию. + * 2. Перезапуск: игрок нажал "Играть снова" — сбрасываем прогресс, сохраняем статистику. + */ +public class NewGameCommand implements Command { + + private final SessionService sessionService; + + public NewGameCommand(SessionService sessionService) { + this.sessionService = sessionService; + } + + @Override + public String execute(HttpServletRequest request, HttpServletResponse response) { + String playerName = request.getParameter("playerName"); + GameSession existingSession = sessionService.getSession(request.getSession()); + + if (playerName != null && !playerName.trim().isEmpty()) { + // Пришло имя — это первый запуск или смена имени + // Создаём новую сессию (или пересоздаём с новым именем) + sessionService.createSession(request.getSession(), playerName.trim()); + } else if (existingSession != null) { + // Имя не пришло, но сессия есть — это "Играть снова" + sessionService.restartGame(request.getSession()); + } else { + // Нет ни имени, ни сессии — возвращаем на старт с ошибкой + request.setAttribute("error", "Please, enter your name!"); + return "/WEB-INF/start-page.jsp"; + } + + return "redirect:/quest"; + } +} diff --git a/src/main/java/com/javarush/ushanov/cmd/QuestCommand.java b/src/main/java/com/javarush/ushanov/cmd/QuestCommand.java new file mode 100644 index 0000000..f41b291 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/cmd/QuestCommand.java @@ -0,0 +1,50 @@ +package com.javarush.ushanov.cmd; + +import com.javarush.ushanov.entity.GameSession; +import com.javarush.ushanov.entity.QuestStep; +import com.javarush.ushanov.service.QuestService; +import com.javarush.ushanov.service.SessionService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Команда для отображения текущего шага квеста. + * + * GET /quest — показывает текущий шаг игроку. + * + * Логика: + * 1. Получаем игровую сессию из HttpSession + * 2. Если сессии нет — перенаправляем на стартовую страницу + * 3. Загружаем текущий шаг квеста + * 4. Кладём данные в request.setAttribute() — JSP их прочитает + * 5. Возвращаем путь к JSP + */ +public class QuestCommand implements Command { + + private final QuestService questService; + private final SessionService sessionService; + + public QuestCommand(QuestService questService, SessionService sessionService) { + this.questService = questService; + this.sessionService = sessionService; + } + + @Override + public String execute(HttpServletRequest request, HttpServletResponse response) { + GameSession gameSession = sessionService.getSession(request.getSession()); + + // Если сессии нет — игрок не начинал игру, отправляем на старт + if (gameSession == null) { + return "redirect:/"; + } + + // Загружаем текущий шаг + QuestStep currentStep = questService.getStep(gameSession.getCurrentStepId()); + + // Кладём данные в request — JSP получит их через ${step} и ${gameSession} + request.setAttribute("step", currentStep); + request.setAttribute("gameSession", gameSession); + + return "/WEB-INF/quest-page.jsp"; + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/ushanov/cmd/StartPageCommand.java b/src/main/java/com/javarush/ushanov/cmd/StartPageCommand.java new file mode 100644 index 0000000..3f169b6 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/cmd/StartPageCommand.java @@ -0,0 +1,17 @@ +package com.javarush.ushanov.cmd; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Команда для отображения стартовой страницы-приветствия. + * Просто показывает JSP с предысторией квеста и формой ввода имени. + */ +public class StartPageCommand implements Command { + + @Override + public String execute(HttpServletRequest request, HttpServletResponse response) { + // Просто возвращаем путь к JSP — контроллер сам сделает forward + return "/WEB-INF/start-page.jsp"; + } +} diff --git a/src/main/java/com/javarush/ushanov/controller/FrontController.java b/src/main/java/com/javarush/ushanov/controller/FrontController.java new file mode 100644 index 0000000..57718f6 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/controller/FrontController.java @@ -0,0 +1,96 @@ +package com.javarush.ushanov.controller; + +import com.javarush.ushanov.cmd.*; +import com.javarush.ushanov.repository.QuestRepository; +import com.javarush.ushanov.service.QuestService; +import com.javarush.ushanov.service.SessionService; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * FrontController — единственный сервлет в приложении. + * + * Паттерн Front Controller: все HTTP-запросы приходят сюда, + * а этот класс решает, какую команду (Command) вызвать. + * + * Важно: мы используем urlPatterns с конкретными путями вместо "/*", + * чтобы Tomcat сам обслуживал статику (CSS, картинки) напрямую, + * не пропуская их через наш сервлет. + */ +@WebServlet(urlPatterns = {"/", "/quest", "/new-game", "/choice"}) +public class FrontController extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(FrontController.class); + + private final Map getCommands = new HashMap<>(); + private final Map postCommands = new HashMap<>(); + + @Override + public void init() { + log.info("Initializing FrontController - creating dependencies and registering commands"); + + QuestRepository questRepository = new QuestRepository(); + QuestService questService = new QuestService(questRepository); + SessionService sessionService = new SessionService(questService); + + // GET-команды: показывают страницы + getCommands.put("/", new StartPageCommand()); + getCommands.put("/quest", new QuestCommand(questService, sessionService)); + + // POST-команды: обрабатывают действия пользователя + postCommands.put("/new-game", new NewGameCommand(sessionService)); + postCommands.put("/choice", new ChoiceCommand(sessionService)); + + log.info("FrontController ready. GET commands: {}, POST commands: {}", + getCommands.size(), postCommands.size()); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + processRequest(request, response, getCommands, "GET"); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + processRequest(request, response, postCommands, "POST"); + } + + private void processRequest(HttpServletRequest request, HttpServletResponse response, + Map commands, String method) + throws ServletException, IOException { + + String uri = request.getRequestURI().substring(request.getContextPath().length()); + log.debug("{} {}", method, uri); + + Command command = commands.get(uri); + + if (command == null) { + log.warn("Command not found for {} {}, returning 404", method, uri); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + String view = command.execute(request, response); + log.debug("Command {} returned view: {}", command.getClass().getSimpleName(), view); + + if (view.startsWith("redirect:")) { + String redirectPath = request.getContextPath() + view.substring("redirect:".length()); + log.debug("Redirect -> {}", redirectPath); + response.sendRedirect(redirectPath); + } else { + log.debug("Forward -> {}", view); + request.getRequestDispatcher(view).forward(request, response); + } + } +} diff --git a/src/main/java/com/javarush/ushanov/entity/GameSession.java b/src/main/java/com/javarush/ushanov/entity/GameSession.java new file mode 100644 index 0000000..323c45c --- /dev/null +++ b/src/main/java/com/javarush/ushanov/entity/GameSession.java @@ -0,0 +1,43 @@ +package com.javarush.ushanov.entity; + +import lombok.Getter; +import lombok.Setter; + +/** + * Данные игровой сессии игрока. + * Этот объект хранится в HttpSession — то есть живёт на сервере + * и привязан к конкретному браузеру/пользователю. + * + * Мы храним здесь: + * - имя игрока (вводится на стартовой странице) + * - id текущего шага квеста + * - количество сыгранных игр + * - количество побед + */ +@Getter +@Setter +public class GameSession { + + private String playerName; + private int currentStepId; + private int gamesPlayed; + private int gamesWon; + + public GameSession(String playerName, int startStepId) { + this.playerName = playerName; + this.currentStepId = startStepId; + this.gamesPlayed = 0; + this.gamesWon = 0; + } + + /** Вызывается при начале новой игры — сбрасывает шаг на стартовый */ + public void startNewGame(int startStepId) { + this.currentStepId = startStepId; + this.gamesPlayed++; + } + + /** Вызывается при победе */ + public void registerWin() { + this.gamesWon++; + } +} diff --git a/src/main/java/com/javarush/ushanov/entity/QuestStep.java b/src/main/java/com/javarush/ushanov/entity/QuestStep.java new file mode 100644 index 0000000..1cf7200 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/entity/QuestStep.java @@ -0,0 +1,35 @@ +package com.javarush.ushanov.entity; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Map; + +/** + * Один шаг (узел) квеста. + * + * Каждый шаг имеет: + * - уникальный id + * - текст вопроса/описания ситуации, который видит игрок + * - статус: PLAYING (обычный шаг), WIN (победа), LOSE (поражение) + * - карту вариантов ответов: текст кнопки -> id следующего шага + * (если шаг финальный — options будет пустой Map) + */ +@Getter +@Builder +public class QuestStep { + + private final int id; + private final String description; + private final StepStatus status; + + /** + * Ключ — текст варианта ответа (то, что видит игрок на кнопке) + * Значение — id шага, на который переходим при выборе этого варианта + */ + private final Map options; + + public boolean isCompleted() { + return status == StepStatus.WIN || status == StepStatus.LOSE; + } +} diff --git a/src/main/java/com/javarush/ushanov/entity/StepStatus.java b/src/main/java/com/javarush/ushanov/entity/StepStatus.java new file mode 100644 index 0000000..efc5909 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/entity/StepStatus.java @@ -0,0 +1,14 @@ +package com.javarush.ushanov.entity; + +/** + * Статус шага квеста. + * + * PLAYING — обычный шаг, игра продолжается + * WIN — финальный шаг с победой + * LOSE — финальный шаг с поражением + */ +public enum StepStatus { + PLAYING, + WIN, + LOSE +} diff --git a/src/main/java/com/javarush/ushanov/exception/QuestStepNotFoundException.java b/src/main/java/com/javarush/ushanov/exception/QuestStepNotFoundException.java new file mode 100644 index 0000000..7fbf310 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/exception/QuestStepNotFoundException.java @@ -0,0 +1,11 @@ +package com.javarush.ushanov.exception; + +/** + * Кастомное исключение — бросается когда шаг квеста не найден. + */ +public class QuestStepNotFoundException extends RuntimeException { + + public QuestStepNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/javarush/ushanov/repository/QuestRepository.java b/src/main/java/com/javarush/ushanov/repository/QuestRepository.java new file mode 100644 index 0000000..8be3a3c --- /dev/null +++ b/src/main/java/com/javarush/ushanov/repository/QuestRepository.java @@ -0,0 +1,182 @@ +package com.javarush.ushanov.repository; + +import com.javarush.ushanov.entity.QuestStep; +import com.javarush.ushanov.entity.StepStatus; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Репозиторий квеста — хранит все шаги квеста в памяти (в Map). + */ +public class QuestRepository { + + /** Все шаги квеста: ключ — id шага, значение — объект QuestStep */ + private final Map steps = new HashMap<>(); + + /** id первого шага, с которого начинается игра */ + public static final int START_STEP_ID = 1; + + public QuestRepository() { + initQuest(); + } + + /** + * Логика квеста в виде дерева шагов. + * + * Сюжет: игрок — пилот разведывательного корабля. + * Корабль получил сигнал бедствия от неизвестной станции. + * + * Дерево решений: + * + * [1] Принять сигнал? + * -> "Принять сигнал" => [2] + * -> "Игнорировать сигнал" => [3] LOSE + * + * [2] Пристыковаться к станции? + * -> "Пристыковаться" => [4] + * -> "Остаться на орбите" => [5] LOSE + * + * [4] Что делать на станции? + * -> "Исследовать реактор" => [6] LOSE + * -> "Найти команду" => [7] + * + * [7] Команда предлагает улететь вместе. Взять всех? + * -> "Взять всех" => [8] WIN + * -> "Взять только капитана" => [9] LOSE + */ + private void initQuest() { + + // Шаг 1 — начало + steps.put(1, QuestStep.builder() + .id(1) + .description("Ты — пилот разведывательного корабля «Аврора». " + + "В пустоте космоса твой бортовой компьютер фиксирует слабый сигнал бедствия. " + + "Источник — заброшенная исследовательская станция «Кеплер-7». " + + "Что ты будешь делать?") + .status(StepStatus.PLAYING) + .options(Map.of( + "Принять сигнал и изменить курс", 2, + "Игнорировать — миссия важнее", 3 + )) + .build()); + + // Шаг 2 — приближаемся к станции + steps.put(2, QuestStep.builder() + .id(2) + .description("Ты изменил курс и приближаешься к станции «Кеплер-7». " + + "Внешне она выглядит повреждённой — несколько секций разгерметизированы. " + + "Однако стыковочный модуль, кажется, работает. " + + "Твои действия?") + .status(StepStatus.PLAYING) + .options(Map.of( + "Пристыковаться и войти на станцию", 4, + "Остаться на орбите и попытаться выйти на связь", 5 + )) + .build()); + + // Шаг 3 — ПОРАЖЕНИЕ: проигнорировал сигнал + steps.put(3, QuestStep.builder() + .id(3) + .description("Ты проигнорировал сигнал и продолжил миссию. " + + "Спустя несколько часов ты узнал из новостей: выжившие со станции «Кеплер-7» " + + "были обнаружены мёртвыми. Ты мог их спасти... " + + "Командование отстранило тебя от полётов.") + .status(StepStatus.LOSE) + .options(Map.of()) + .build()); + + // Шаг 4 — на станции + steps.put(4, QuestStep.builder() + .id(4) + .description("Ты на борту станции. Воздух разреженный, аварийное освещение мигает. " + + "Датчики показывают двух выживших в жилом модуле. " + + "Но также фиксируется нестабильность реактора. " + + "Что исследовать первым?") + .status(StepStatus.PLAYING) + .options(Map.of( + "Срочно проверить реактор", 6, + "Найти выживших в жилом модуле", 7 + )) + .build()); + + // Шаг 5 — ПОРАЖЕНИЕ: остался на орбите + steps.put(5, QuestStep.builder() + .id(5) + .description("Ты пытался выйти на связь несколько часов, но ответа не было. " + + "Пока ты медлил, реактор станции вышел из строя. " + + "Мощный взрыв уничтожил станцию вместе со всеми выжившими. " + + "Нужно было действовать быстрее.") + .status(StepStatus.LOSE) + .options(Map.of()) + .build()); + + // Шаг 6 — ПОРАЖЕНИЕ: пошёл к реактору + steps.put(6, QuestStep.builder() + .id(6) + .description("Ты направился к реактору, но не успел дойти — " + + "перегрузка системы вызвала разрыв трубопровода. " + + "Радиоактивный пар заполнил коридор. " + + "Ты получил критическую дозу облучения. " + + "Выживших найти не удалось.") + .status(StepStatus.LOSE) + .options(Map.of()) + .build()); + + // Шаг 7 — нашли выживших + steps.put(7, QuestStep.builder() + .id(7) + .description("Ты нашёл двух выживших: капитана Марину Волкову и бортинженера Алекса Рида. " + + "Они истощены, но в сознании. Капитан говорит: " + + "«Нас трое — мой напарник застрял в заблокированном отсеке, " + + "но корабль может принять только двух пассажиров». " + + "Что решаешь?") + .status(StepStatus.PLAYING) + .options(Map.of( + "Забрать обоих — найдём способ", 8, + "Взять только капитана — она важнее для расследования", 9 + )) + .build()); + + // Шаг 8 — ПОБЕДА + steps.put(8, QuestStep.builder() + .id(8) + .description("Ты нашёл решение: один из выживших может разместиться в грузовом отсеке. " + + "Немного тесновато, зато все живы! " + + "«Аврора» отстыковалась от станции за минуту до того, как реактор взорвался. " + + "Оба выживших доставлены на базу. " + + "Командование объявило тебя героем. Миссия выполнена!") + .status(StepStatus.WIN) + .options(Map.of()) + .build()); + + // Шаг 9 — ПОРАЖЕНИЕ: бросил инженера + steps.put(9, QuestStep.builder() + .id(9) + .description("Ты взял только капитана и улетел. " + + "Бортинженер Алекс Рид погиб при взрыве реактора. " + + "Капитан Волкова так и не простила тебя. " + + "На базе тебя ждал трибунал за нарушение устава: " + + "«Не оставляй своих». Конец карьеры.") + .status(StepStatus.LOSE) + .options(Map.of()) + .build()); + } + + /** + * Получить шаг по id. + */ + public Optional findById(int id) { + return Optional.ofNullable(steps.get(id)); + } + + public int getStartStepId() { + return START_STEP_ID; + } + + /** Общее количество шагов в квесте (для тестов) */ + public int size() { + return steps.size(); + } +} diff --git a/src/main/java/com/javarush/ushanov/service/QuestService.java b/src/main/java/com/javarush/ushanov/service/QuestService.java new file mode 100644 index 0000000..0417e32 --- /dev/null +++ b/src/main/java/com/javarush/ushanov/service/QuestService.java @@ -0,0 +1,44 @@ +package com.javarush.ushanov.service; + +import com.javarush.ushanov.entity.QuestStep; +import com.javarush.ushanov.exception.QuestStepNotFoundException; +import com.javarush.ushanov.repository.QuestRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class QuestService { + + private static final Logger log = LoggerFactory.getLogger(QuestService.class); + + private final QuestRepository questRepository; + + public QuestService(QuestRepository questRepository) { + this.questRepository = questRepository; + } + + public QuestStep getStep(int stepId) { + return questRepository.findById(stepId) + .orElseThrow(() -> { + // ERROR — что-то пошло не так, нужно разобраться + log.error("Quest step with id={} not found", stepId); + return new QuestStepNotFoundException("Quest step with id=" + stepId + " not found"); + }); + } + + public int getStartStepId() { + return questRepository.getStartStepId(); + } + + public int getNextStepId(int currentStepId, String chosenOption) { + QuestStep currentStep = getStep(currentStepId); + Integer nextStepId = currentStep.getOptions().get(chosenOption); + if (nextStepId == null) { + log.error("Choice option '{}' not found for step id={}", chosenOption, currentStepId); + throw new QuestStepNotFoundException( + "Choice option '" + chosenOption + "' not found for step id=" + currentStepId + ); + } + log.debug("Transition: step {} -> step {} (choice: '{}')", currentStepId, nextStepId, chosenOption); + return nextStepId; + } +} diff --git a/src/main/java/com/javarush/ushanov/service/SessionService.java b/src/main/java/com/javarush/ushanov/service/SessionService.java new file mode 100644 index 0000000..c07a0bb --- /dev/null +++ b/src/main/java/com/javarush/ushanov/service/SessionService.java @@ -0,0 +1,61 @@ +package com.javarush.ushanov.service; + +import com.javarush.ushanov.entity.GameSession; +import com.javarush.ushanov.entity.StepStatus; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionService { + + private static final Logger log = LoggerFactory.getLogger(SessionService.class); + + public static final String SESSION_KEY = "gameSession"; + + private final QuestService questService; + + public SessionService(QuestService questService) { + this.questService = questService; + } + + public GameSession createSession(HttpSession httpSession, String playerName) { + GameSession gameSession = new GameSession(playerName, questService.getStartStepId()); + gameSession.startNewGame(questService.getStartStepId()); + httpSession.setAttribute(SESSION_KEY, gameSession); + log.info("New session created for player '{}'", playerName); + return gameSession; + } + + public GameSession getSession(HttpSession httpSession) { + return (GameSession) httpSession.getAttribute(SESSION_KEY); + } + + public void processChoice(HttpSession httpSession, String chosenOption) { + GameSession gameSession = getSession(httpSession); + if (gameSession == null) { + log.warn("processChoice called without active session"); + return; + } + + int nextStepId = questService.getNextStepId(gameSession.getCurrentStepId(), chosenOption); + gameSession.setCurrentStepId(nextStepId); + + var nextStep = questService.getStep(nextStepId); + if (nextStep.getStatus() == StepStatus.WIN) { + gameSession.registerWin(); + log.info("Player '{}' won! Total wins: {}", + gameSession.getPlayerName(), gameSession.getGamesWon()); + } else if (nextStep.getStatus() == StepStatus.LOSE) { + log.info("Player '{}' lost at step {}", gameSession.getPlayerName(), nextStepId); + } + } + + public void restartGame(HttpSession httpSession) { + GameSession gameSession = getSession(httpSession); + if (gameSession != null) { + gameSession.startNewGame(questService.getStartStepId()); + log.info("Player '{}' restarted the game. Games played: {}", + gameSession.getPlayerName(), gameSession.getGamesPlayed()); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..a4800b6 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + UTF-8 + %d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/src/main/webapp/WEB-INF/edit-user.jsp b/src/main/webapp/WEB-INF/edit-user.jsp deleted file mode 100644 index f274104..0000000 --- a/src/main/webapp/WEB-INF/edit-user.jsp +++ /dev/null @@ -1,73 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@include file="head.jsp" %> - - - - - - - Edit user: - - - - Login - - - min 3 symbols - - - - - - Password - - - min 8 symb - - - - - - - Role - - - - ${role} - - - - - - - - Operation - - - Create - - - Update - - - - - - - - - - diff --git a/src/main/webapp/WEB-INF/error-page.jsp b/src/main/webapp/WEB-INF/error-page.jsp new file mode 100644 index 0000000..60a7363 --- /dev/null +++ b/src/main/webapp/WEB-INF/error-page.jsp @@ -0,0 +1,37 @@ +<%-- + Created by IntelliJ IDEA. + User: ushan + Date: 26.02.2026 + Time: 1:50 + To change this template use File | Settings | File Templates. +--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %> +<%@ taglib prefix="c" uri="jakarta.tags.core" %> + +<%@ include file="header.jsp" %> + + + + + + + ${pageContext.errorData.statusCode} + + Ошибка + + + Системный сбой + + Бортовой компьютер зафиксировал критическую ошибку системы навигации. + Запрошенный маршрут не существует или временно недоступен. + + + ← Вернуться на главную + + + + +