diff --git a/.fastRequest/config/fastRequestCurrentProjectEnvironment.json b/.fastRequest/config/fastRequestCurrentProjectEnvironment.json new file mode 100644 index 0000000..9759716 --- /dev/null +++ b/.fastRequest/config/fastRequestCurrentProjectEnvironment.json @@ -0,0 +1,6 @@ +{ + "apifoxRelationMap":{}, + "apifoxServerMap":{}, + "environment":{}, + "pmRelationMap":{} +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4239a9 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# ProjectPantera - Java веб-приложение + +## Обзор + +ProjectPantera - это Java-веб-приложение для проведения технических собеседований и тестов, в основном ориентированное на темы программирования на Java. Приложение следует архитектурному паттерну MVC и использует технологический стек на основе сервлетов. + +## Архитектура пакетов + +### 📁 **Основные пакеты приложения** + +#### **`com.javarush.zyibin.controllers`** +Компоненты веб-слоя, отвечающие за обработку HTTP-запросов и ответов. +- **Основные контроллеры**: `LoginServlet`, `ProfileServlet`, `QuestionServlet`, `ResultServlet` и т.д. +- **Подпакет admin**: `controllers.admin` - административная функциональность (`AdminStatisticsServlet`, `AdminUserServlet` и т.д.) + +#### **`com.javarush.zyibin.service`** +Слой бизнес-логики, содержащий сервисы приложения. +- **Пользовательские сервисы**: `UserService`, `AuthenticationService`, `RegistrationService` +- **Сервисы статистики**: `UserStatisticsService`, `UserTestStatisticsService` +- **Другие сервисы**: `QuestionService`, `AvatarService` + +#### **`com.javarush.zyibin.repository`** +Слой доступа к данным для операций персистентности. +- **Репозитории**: `UserRepository`, `TestResultRepository`, `QuestionRepository` +- **Реализации**: `InMemoryUserRepository`, `InMemoryTestResultRepository` + +#### **`com.javarush.zyibin.model`** +Классы доменной модели, представляющие основные бизнес-сущности. +- **Сущности**: `User`, `TestResult`, `Question`, `Topic`, `Role`, `InterviewState` + +### 📁 **Объекты передачи данных (DTO)** + +#### **`com.javarush.zyibin.dto`** +Объекты передачи данных и value objects для представления данных. +- **DTO для статистики**: `UserStats`, `TopicStats`, `UserTestStats`, `UserTopicStats` +- **Базовые классы**: `BaseStats` - абстрактный базовый класс для статистики + +### 📁 **Вспомогательные пакеты** + +#### **`com.javarush.zyibin.util`** +Утилитные классы и вспомогательные функции. +- **Утилиты**: `PasswordUtil`, `TopicUtils`, `ValidationFactory` + +#### **`com.javarush.zyibin.validation`** +Компоненты валидации входных данных. +- **Валидаторы**: `UserValidation`, `QuestionValidator` + +#### **`com.javarush.zyibin.exception`** +Пользовательские классы исключений. +- **Исключения**: `AuthenticationException`, `ValidationException` + +#### **`com.javarush.zyibin.filter`** +Сервлет-фильтры для обработки запросов. +- **Фильтры**: `AuthFilter`, `AdminFilter` + +#### **`com.javarush.zyibin.session`** +Утилиты управления сессией. +- **Классы**: `SessionUtils`, `SessionKeys` + +#### **`com.javarush.zyibin.source`** +Реализации источников данных. +- **Источники**: `QuestionSource`, `FileQuestionSource` + +#### **`com.javarush.zyibin.handler`** +Компоненты обработки ошибок и запросов. +- **Обработчики**: `ErrorHandler`, `RequestHandler` + +#### **`com.javarush.zyibin.config`** +Компоненты конфигурации и инициализации. +- **Конфигурация**: `ApplicationConfig`, `AppInitListener` + +## Принципы архитектуры + +### **Разделение ответственности** +- **Контроллеры**: Обрабатывают только HTTP-запросы/ответы +- **Сервисы**: Содержат бизнес-логику +- **Репозитории**: Управляют персистентностью данных +- **Модели**: Представляют доменные сущности +- **DTO**: Передают данные между слоями + +### **Организация пакетов** +- **Функциональная группировка**: Связанные классы сгруппированы вместе +- **Разделение слоев**: Чёткое различие между представлением, бизнес-логикой и данными +- **Разделение утилит**: Вспомогательные классы в выделенных пакетах + +### **Используемые паттерны проектирования** +- **MVC Pattern**: архитектура Model-View-Controller +- **Factory Pattern**: `ValidationFactory` для создания валидаторов +- **Repository Pattern**: абстракция доступа к данным +- **DTO Pattern**: объекты передачи данных для коммуникации между слоями + +## Технологический стек + +- **Java 17** - язык программирования +- **Servlet API** - веб-фреймворк +- **JSP** - технология представлений +- **Jackson** - обработка JSON +- **SLF4J** - фреймворк логирования +- **JUnit 5** - фреймворк тестирования +- **Mockito** - фреймворк мокирования +- **Maven** - инструмент сборки + +## Ключевые возможности + +1. **Управление пользователями**: регистрация, аутентификация, управление профилем +2. **Система тестов**: тематические технические тесты с вопросами +3. **Статистика**: комплексная статистика тестов и пользователей +4. **Административная панель**: административная функциональность для управления пользователями +5. **Управление сессией**: отслеживание состояния собеседования +6. **Валидация**: валидация входных данных для пользователей и вопросов + +## Последние улучшения + +### **Реструктуризация пакетов** +- ✅ Перемещены все классы статистики в пакет `dto` +- ✅ Консолидирован `ValidationFactory` в пакет `util` +- ✅ Перемещен `InterviewState` в пакет `model` +- ✅ Устранены избыточные небольшие пакеты +- ✅ Улучшена организация и консистентность пакетов + +### **Качество кода** +- ✅ Устранено дублирование кода с помощью абстрактного класса `BaseStats` +- ✅ Централизована конверсия кодов тем в `TopicUtils` +- ✅ Улучшена организация импортов +- ✅ Повышена поддерживаемость + +## Руководство по разработке + +1. **Следуйте конвенциям пакетов**: размещайте классы в соответствующих пакетах в зависимости от функциональности +2. **Поддерживайте разделение**: не размещайте бизнес-логику в контроллерах +3. **Используйте DTO**: передавайте данные между слоями с помощью соответствующих DTO +4. **Реализуйте валидацию**: валидируйте входные данные на уровне сервисов +5. **Обрабатывайте исключения**: используйте пользовательские исключения для сценариев ошибок +6. **Пишите тесты**: поддерживайте покрытие тестами всех компонентов + +## Сборка и запуск + +```bash +# Скомпилировать проект +mvn compile + +# Запустить тесты +mvn test + +# Собрать WAR-файл +mvn package + +# Запустить приложение (требуется сервлет-контейнер) +# Развернуть target/project-pantera-1.0-SNAPSHOT.war в Tomcat или аналогичный +``` + +## Структура проекта + +``` +src/ +├── main/ +│ ├── java/com/javarush/zyibin/ +│ │ ├── controllers/ # Веб-слой +│ │ ├── service/ # Бизнес-логика +│ │ ├── repository/ # Доступ к данным +│ │ ├── model/ # Доменные сущности +│ │ ├── dto/ # Объекты передачи данных +│ │ ├── util/ # Утилиты +│ │ ├── validation/ # Валидация +│ │ ├── exception/ # Пользовательские исключения +│ │ ├── filter/ # Сервлет-фильтры +│ │ ├── session/ # Управление сессией +│ │ ├── source/ # Источники данных +│ │ ├── handler/ # Обработка ошибок +│ │ └── config/ # Конфигурация +│ └── resources/ +│ └── questions/ # Данные тестовых вопросов +└── test/ # Тестовые классы +``` + +--- + +*Этот документ описывает текущее состояние архитектуры приложения ProjectPantera после последнего рефакторинга.* diff --git a/pom.xml b/pom.xml index cda3199..2ce9274 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,12 @@ org.glassfish.web jakarta.servlet.jsp.jstl - + + org.junit.jupiter + junit-jupiter + 5.10.1 + test + org.junit.jupiter junit-jupiter-api @@ -64,6 +69,40 @@ junit-jupiter-engine test + + org.mockito + mockito-core + 5.8.0 + test + + + org.mockito + mockito-junit-jupiter + 5.8.0 + test + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-web + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + @@ -86,6 +125,30 @@ maven-war-plugin 3.4.0 + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + verify + + report + + + + \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/app/cmd/Command.java b/src/main/java/com/javarush/khmelov/app/cmd/Command.java deleted file mode 100644 index 644bb2c..0000000 --- a/src/main/java/com/javarush/khmelov/app/cmd/Command.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.javarush.khmelov.app.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(); - } - - //EditUser -> edit-user -> edit_edit - 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/app/cmd/EditUser.java b/src/main/java/com/javarush/khmelov/app/cmd/EditUser.java deleted file mode 100644 index d470acf..0000000 --- a/src/main/java/com/javarush/khmelov/app/cmd/EditUser.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.javarush.khmelov.app.cmd; - -import com.javarush.khmelov.app.config.Winter; -import com.javarush.khmelov.app.entity.Role; -import com.javarush.khmelov.app.entity.User; -import com.javarush.khmelov.app.service.UserService; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -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; - -public class EditUser implements Command { - - private final UserService userService; - - public EditUser(UserService userService) { - this.userService = userService; - } - - - @Override - public String doGet(HttpServletRequest req) { - String strId = req.getParameter("id"); - if (strId != null && !strId.isEmpty()) { - long id = Long.parseLong(strId); - User user = userService.get(id).orElseThrow(); - req.setAttribute("user", user); - } - return getView(); - } - - @Override - public String doPost(HttpServletRequest req) { - String strId = req.getParameter("id"); - Long id = strId == null || strId.isEmpty() - ? null - : Long.parseLong(strId); - User user = User.builder() - .id(id) - .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) { - userService.update(user); - } else { - throw new RuntimeException("incorrect form data"); - } - return getView()+"?id=" + user.getId(); - } -} diff --git a/src/main/java/com/javarush/khmelov/app/cmd/ListUser.java b/src/main/java/com/javarush/khmelov/app/cmd/ListUser.java deleted file mode 100644 index ab46ef9..0000000 --- a/src/main/java/com/javarush/khmelov/app/cmd/ListUser.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.javarush.khmelov.app.cmd; - -import com.javarush.khmelov.app.entity.User; -import com.javarush.khmelov.app.service.UserService; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Collection; - -public class ListUser implements Command { - - private final UserService userService; - - public ListUser(UserService userService) { - this.userService = userService; - } - - @Override - public String doGet(HttpServletRequest req) { - Collection users = userService.getAll(); - req.setAttribute("users", users); - return getView(); - } -} diff --git a/src/main/java/com/javarush/khmelov/app/cmd/StartPage.java b/src/main/java/com/javarush/khmelov/app/cmd/StartPage.java deleted file mode 100644 index 51ebf56..0000000 --- a/src/main/java/com/javarush/khmelov/app/cmd/StartPage.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.app.cmd; - -public class StartPage implements Command { - -} diff --git a/src/main/java/com/javarush/khmelov/app/config/Winter.java b/src/main/java/com/javarush/khmelov/app/config/Winter.java deleted file mode 100644 index 7e47cca..0000000 --- a/src/main/java/com/javarush/khmelov/app/config/Winter.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.javarush.khmelov.app.config; - - -import lombok.SneakyThrows; - -import java.lang.reflect.Constructor; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class Winter { - - private Winter() { - - } - - public static Map, Object> beans = new ConcurrentHashMap<>(); - - @SuppressWarnings("unchecked") - @SneakyThrows - public static T find(Class clazz) { - if (!beans.containsKey(clazz)) { - Constructor constructor = clazz.getConstructors()[0]; - Class[] parameterTypes = constructor.getParameterTypes(); - Object[] parameters = new Object[parameterTypes.length]; - for (int i = 0; i < parameterTypes.length; i++) { - parameters[i] = find(parameterTypes[i]); - } - Object bean = constructor.newInstance(parameters); - beans.put(clazz, bean); - } - return (T) beans.get(clazz); - - } -} diff --git a/src/main/java/com/javarush/khmelov/app/controller/FrontController.java b/src/main/java/com/javarush/khmelov/app/controller/FrontController.java deleted file mode 100644 index 2bee710..0000000 --- a/src/main/java/com/javarush/khmelov/app/controller/FrontController.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.javarush.khmelov.app.controller; - -import com.javarush.khmelov.app.cmd.Command; -import com.javarush.khmelov.app.config.Winter; -import com.javarush.khmelov.app.entity.Role; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -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(urlPatterns = {"/"}, loadOnStartup = 1) -public class FrontController extends HttpServlet { - - private final HttpResolver httpResolver = Winter.find(HttpResolver.class); - - @Override - public void init(ServletConfig config) throws ServletException { - ServletContext appScope = config.getServletContext(); - appScope.setAttribute("roles", Role.values()); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Command command = httpResolver.resolve(req); - if (command != null) { - String view = command.doGet(req); - String jsp = getJsp(view); - req.getRequestDispatcher(jsp).forward(req, resp); - } else { - //надо не super вызывать, а default - req.getServletContext().getNamedDispatcher("default").forward(req, resp); - } - } - - 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/app/controller/HttpResolver.java b/src/main/java/com/javarush/khmelov/app/controller/HttpResolver.java deleted file mode 100644 index 1d553c6..0000000 --- a/src/main/java/com/javarush/khmelov/app/controller/HttpResolver.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.javarush.khmelov.app.controller; - -import com.javarush.khmelov.app.cmd.Command; -import com.javarush.khmelov.app.config.Winter; -import jakarta.servlet.http.HttpServletRequest; - -public class HttpResolver { - - public Command resolve(HttpServletRequest req) { - try { - // /edit-user?id=18#ok - String uri = req.getRequestURI(); - uri=uri.equals("/") ? "/start-page" : uri; - String cmdKebabName = uri.split("[/?#]")[1]; - String simpleName = convertKebabStyleToCamelCase(cmdKebabName); - String packageName = Command.class.getPackageName(); - String fqName=packageName+"."+simpleName; - Class cmdClass = Class.forName(fqName); - return (Command) Winter.find(cmdClass); - } catch (ClassNotFoundException e) { - return null; - } - } - - private static 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/app/entity/Role.java b/src/main/java/com/javarush/khmelov/app/entity/Role.java deleted file mode 100644 index 02fdbd4..0000000 --- a/src/main/java/com/javarush/khmelov/app/entity/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.app.entity; - -public enum Role { - USER, ADMIN, GUEST -} diff --git a/src/main/java/com/javarush/khmelov/app/entity/User.java b/src/main/java/com/javarush/khmelov/app/entity/User.java deleted file mode 100644 index fd49995..0000000 --- a/src/main/java/com/javarush/khmelov/app/entity/User.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.app.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/app/repository/Repository.java b/src/main/java/com/javarush/khmelov/app/repository/Repository.java deleted file mode 100644 index 80b4a6b..0000000 --- a/src/main/java/com/javarush/khmelov/app/repository/Repository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.javarush.khmelov.app.repository; - -import com.javarush.khmelov.app.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/app/repository/UserRepository.java b/src/main/java/com/javarush/khmelov/app/repository/UserRepository.java deleted file mode 100644 index bab8727..0000000 --- a/src/main/java/com/javarush/khmelov/app/repository/UserRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.javarush.khmelov.app.repository; - -import com.javarush.khmelov.app.entity.Role; -import com.javarush.khmelov.app.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/app/service/UserService.java b/src/main/java/com/javarush/khmelov/app/service/UserService.java deleted file mode 100644 index 0993326..0000000 --- a/src/main/java/com/javarush/khmelov/app/service/UserService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.javarush.khmelov.app.service; - -import com.javarush.khmelov.app.entity.User; -import com.javarush.khmelov.app.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/khmelov/lesson16/p01_singleton/GoF_01_Singleton.java b/src/main/java/com/javarush/khmelov/lesson16/p01_singleton/GoF_01_Singleton.java deleted file mode 100644 index eda4667..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p01_singleton/GoF_01_Singleton.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.javarush.khmelov.lesson16.p01_singleton; - -public class GoF_01_Singleton { - private static GoF_01_Singleton instance; - - //конструктор - private GoF_01_Singleton() { - } - - //получим объект (если нужно создадим его) - public static GoF_01_Singleton getInstance() { - if (instance == null) { - instance = new GoF_01_Singleton(); - } - return instance; - } - - public static void main(String[ ] args) { - System.out.println(GoF_01_Singleton.getInstance()); - System.out.println(GoF_01_Singleton.getInstance()); - System.out.println(GoF_01_Singleton.getInstance()); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p01_singleton/GoF_02_Singleton.java b/src/main/java/com/javarush/khmelov/lesson16/p01_singleton/GoF_02_Singleton.java deleted file mode 100644 index 46b7d60..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p01_singleton/GoF_02_Singleton.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.javarush.khmelov.lesson16.p01_singleton; - -public class GoF_02_Singleton { - private static volatile GoF_02_Singleton instance; - //конструктор класса - private GoF_02_Singleton() { - } - //метод доступа к экземпляру - public static GoF_02_Singleton getInstance() { - GoF_02_Singleton localInstance = instance; //попытка получить объект - if (localInstance == null) { //первая проверка (для скорости) - synchronized (GoF_02_Singleton.class) { - localInstance = instance; //вторая проверка для надежности - if (localInstance == null) { - instance = localInstance = new GoF_02_Singleton(); //создание - } - } - } - return localInstance; //возврат - } - public static void main(String[ ] args) { - System.out.println(GoF_02_Singleton.getInstance()); - System.out.println(GoF_02_Singleton.getInstance()); - System.out.println(GoF_02_Singleton.getInstance()); - } - -} - - diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/GoF_03_Factory.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/GoF_03_Factory.java deleted file mode 100644 index 19d450f..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/GoF_03_Factory.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory; - -public class GoF_03_Factory { - public static void main(String[] args) { - // an array of creators - Creator[] creators = {new CreatorScalar(), new CreatorVec()}; - // iterate over creators and create products - for (Creator creator : creators) { - Var product = creator.factoryMethod(); - System.out.printf("Created {%s}\n", product.getClass()); - } - } -} - -class Var { } - -class VarScalar extends Var { } - -class VarVector extends Var { } - -abstract class Creator { - public abstract Var factoryMethod(); -} - -class CreatorScalar extends Creator { - @Override - public Var factoryMethod() { return new VarScalar(); } -} - -class CreatorVec extends Creator { - @Override - public Var factoryMethod() { return new VarVector(); } -} - diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Car.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Car.java deleted file mode 100644 index 800c47e..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Car.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -// можно использовать абстрактный класс, если нужно задать реализацию метода по-умолчанию -interface Car { - - void drive(); - void stop(); - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/CarSelector.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/CarSelector.java deleted file mode 100644 index fec99d4..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/CarSelector.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -// фабрика по созданию автомобилей -class CarSelector { - - // фабричный метод, который создает нужный автомобиль - Car getCar(RoadType roadType) { - return switch (roadType) { - case CITY -> new Mercedes(); - case OFF_ROAD -> new Geep(); - case ROAD -> new Crossover(); - }; - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Crossover.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Crossover.java deleted file mode 100644 index 92b983d..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Crossover.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -class Crossover extends Geep { - - public void newFunction(){ - System.out.println("new function"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Geep.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Geep.java deleted file mode 100644 index a2e9920..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Geep.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -class Geep implements Car{ - - @Override - public void drive() { - System.out.println("Drive speed 50 km/h"); - } - - @Override - public void stop() { - System.out.println("Stopped at 5 sec"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Mercedes.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Mercedes.java deleted file mode 100644 index 54e7229..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Mercedes.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -class Mercedes implements Car{ - - @Override - public void drive() { - System.out.println("Drive speed 150 km/h"); - } - - @Override - public void stop() { - System.out.println("Stopped at 1 sec"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/RoadType.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/RoadType.java deleted file mode 100644 index bc79df0..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/RoadType.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -enum RoadType { - - CITY, - OFF_ROAD, - ROAD - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Start.java b/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Start.java deleted file mode 100644 index c3d79bd..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p02_factory/sample/Start.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.javarush.khmelov.lesson16.p02_factory.sample; - -class Start { - - public static void main(String[] args) { - - CarSelector carSelector = new CarSelector(); - - Car car = carSelector.getCar(RoadType.CITY); - car.drive(); - car.stop(); - - car = carSelector.getCar(RoadType.OFF_ROAD); - car.drive(); - car.stop(); - - - car = carSelector.getCar(RoadType.ROAD); - - - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/Start.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/Start.java deleted file mode 100644 index 742a52d..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/Start.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.factory.impl.BelorussianFactory; -import com.javarush.khmelov.lesson16.p03_abstract_factory.factory.impl.USAFactory; -import com.javarush.khmelov.lesson16.p03_abstract_factory.factory.interfaces.TransportFactory; - -public class Start { - public static void main(String[] args) { - boolean home = Math.random()>0.5; - TransportFactory factory; - if (home){ - factory = new BelorussianFactory(); - }else{ - factory = new USAFactory(); - } - factory.createAircraft().flight(); - factory.createCar().drive(); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/BelorussianFactory.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/BelorussianFactory.java deleted file mode 100644 index 6d99d5c..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/BelorussianFactory.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.factory.impl; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.factory.interfaces.TransportFactory; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.aircraft.TU134; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.car.Maz; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Aircraft; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Car; - -// российские транспортные средства -public class BelorussianFactory implements TransportFactory{ - - @Override - public Car createCar() { - return new Maz(); - } - - @Override - public Aircraft createAircraft() { - return new TU134(); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/USAFactory.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/USAFactory.java deleted file mode 100644 index 5634074..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/impl/USAFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.factory.impl; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.factory.interfaces.TransportFactory; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.aircraft.Boeing747; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.car.Porsche; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Aircraft; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Car; - -// американские транспортные средства -public class USAFactory implements TransportFactory{ - @Override - public Car createCar() { - return new Porsche(); - } - - @Override - public Aircraft createAircraft() { - return new Boeing747(); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/interfaces/TransportFactory.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/interfaces/TransportFactory.java deleted file mode 100644 index 0572bac..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/factory/interfaces/TransportFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.factory.interfaces; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Aircraft; -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Car; - -// фабрика по созданию транспортных средств -public interface TransportFactory { // что фабрика будет производить - Car createCar();// автомобили - Aircraft createAircraft(); // самолеты - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/Boeing747.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/Boeing747.java deleted file mode 100644 index 2edfea1..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/Boeing747.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.aircraft; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Aircraft; - -public class Boeing747 implements Aircraft{ - @Override - public void flight() { - System.out.println("Boeing747 flight!"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/TU134.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/TU134.java deleted file mode 100644 index 8d00b57..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/aircraft/TU134.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.aircraft; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Aircraft; - -public class TU134 implements Aircraft{ - @Override - public void flight() { - System.out.println("TU-134 flight!"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Maz.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Maz.java deleted file mode 100644 index 9780d07..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Maz.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.car; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Car; - -public class Maz implements Car { - @Override - public void drive() { - System.out.println("Maz drive"); - } - - @Override - public void stop() { - System.out.println("Maz stopped"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Porsche.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Porsche.java deleted file mode 100644 index fbb7231..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/impl/car/Porsche.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.impl.car; - -import com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces.Car; - -public class Porsche implements Car{ - @Override - public void drive() { - System.out.println("Drive speed 150 km/h"); - } - - @Override - public void stop() { - System.out.println("Stopped at 1 sec"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Aircraft.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Aircraft.java deleted file mode 100644 index 8ff9619..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Aircraft.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces; - -public interface Aircraft { - void flight(); -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Car.java b/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Car.java deleted file mode 100644 index de194c0..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p03_abstract_factory/transport/interfaces/Car.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javarush.khmelov.lesson16.p03_abstract_factory.transport.interfaces; - -// можно использовать абстрактный класс, если нужно задать реализацию метода по-умолчанию -public interface Car { - void drive(); - void stop(); -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/HawaiianPizzaBuilder.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/HawaiianPizzaBuilder.java deleted file mode 100644 index c5b5952..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/HawaiianPizzaBuilder.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** "ConcreteBuilder" */ -class HawaiianPizzaBuilder extends PizzaBuilder { - public void buildDough() { pizza.setDough("cross"); } - public void buildSauce() { pizza.setSauce("mild"); } - public void buildTopping() { pizza.setTopping("ham+pineapple"); } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Pizza.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Pizza.java deleted file mode 100644 index 1a1dca9..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Pizza.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** "Product" */ -class Pizza { - private String dough=""; - private String sauce=""; - private String topping=""; - - //тут сеттеры дружественные, но это обычно это public методы - void setDough(String dough) { this.dough = dough; } - void setSauce(String sauce) { this.sauce = sauce; } - void setTopping(String topping) { this.topping = topping; } - - - @Override - public String toString() { - return "Pizza{" + - "dough='" + dough + '\'' + - ", sauce='" + sauce + '\'' + - ", topping='" + topping + '\'' + - '}'; - } -} - - diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/PizzaBuilder.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/PizzaBuilder.java deleted file mode 100644 index 659cf3a..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/PizzaBuilder.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** "Abstract Builder" */ -abstract class PizzaBuilder { - Pizza pizza; - - Pizza getPizza() { return pizza; } - void createNewPizzaProduct() { pizza = new Pizza(); } - - public abstract void buildDough(); - public abstract void buildSauce(); - public abstract void buildTopping(); -} - diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/RunnerBuilder.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/RunnerBuilder.java deleted file mode 100644 index 5778786..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/RunnerBuilder.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** - * A customer ordering a product. - */ -public class RunnerBuilder { - public static void main(String[] args) { - Waiter waiter = new Waiter(); - PizzaBuilder pizzaBuilder = Math.random() > 0.5 ? new HawaiianPizzaBuilder() : new SpicyPizzaBuilder(); - waiter.setPizzaBuilder(pizzaBuilder); - waiter.constructPizza(); - - Pizza pizza = waiter.getPizza(); - System.out.println(pizza); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/SpicyPizzaBuilder.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/SpicyPizzaBuilder.java deleted file mode 100644 index 5c68447..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/SpicyPizzaBuilder.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** "ConcreteBuilder" */ -class SpicyPizzaBuilder extends PizzaBuilder { - public void buildDough() { pizza.setDough("pan baked"); } - public void buildSauce() { pizza.setSauce("hot"); } - public void buildTopping() { pizza.setTopping("pepperoni+salami"); } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Waiter.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Waiter.java deleted file mode 100644 index cb9ed33..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/Waiter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder; - -/** "Director" */ -class Waiter { - private PizzaBuilder pizzaBuilder; - - void setPizzaBuilder(PizzaBuilder pb) { pizzaBuilder = pb; } - Pizza getPizza() { return pizzaBuilder.getPizza(); } - - void constructPizza() { - pizzaBuilder.createNewPizzaProduct(); - pizzaBuilder.buildDough(); - pizzaBuilder.buildSauce(); - pizzaBuilder.buildTopping(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/Client.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/Client.java deleted file mode 100644 index cd3d019..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/Client.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder.modern; - -public class Client { - public static void main(String[] args) { - ModernPizza pizza = new ModernPizza.ModernPizzaBuilder() - .sauce("tomato") - .dough("thin") - .topping("pepperoni") - .build(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/ModernPizza.java b/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/ModernPizza.java deleted file mode 100644 index d6a6df5..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p04_builder/modern/ModernPizza.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.javarush.khmelov.lesson16.p04_builder.modern; - -public class ModernPizza { - private String dough; - private String sauce; - private String topping; - - ModernPizza(String dough, String sauce, String topping) { - this.dough = dough; - this.sauce = sauce; - this.topping = topping; - } - - public static ModernPizzaBuilder builder() { - return new ModernPizzaBuilder(); - } - - public static class ModernPizzaBuilder { - private String dough; - private String sauce; - private String topping; - - ModernPizzaBuilder() { - } - - public ModernPizzaBuilder dough(String dough) { - this.dough = dough; - return this; - } - - public ModernPizzaBuilder sauce(String sauce) { - this.sauce = sauce; - return this; - } - - public ModernPizzaBuilder topping(String topping) { - this.topping = topping; - return this; - } - - public ModernPizza build() { - return new ModernPizza(this.dough, this.sauce, this.topping); - } - - public String toString() { - return "ModernPizza.ModernPizzaBuilder(dough=" + this.dough + ", sauce=" + this.sauce + ", topping=" + this.topping + ")"; - } - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/Start.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/Start.java deleted file mode 100644 index 63a0519..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/Start.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator; - -import com.javarush.khmelov.lesson16.p05_decorator.decorators.ColorDecorator; -import com.javarush.khmelov.lesson16.p05_decorator.decorators.MagicDecorator; -import com.javarush.khmelov.lesson16.p05_decorator.objects.Ork; -import com.javarush.khmelov.lesson16.p05_decorator.objects.Rider; -import com.javarush.khmelov.lesson16.p05_decorator.objects.Soldier; -import com.javarush.khmelov.lesson16.p05_decorator.objects.Unit; - -public class Start { - - - public static void main(String[] args) { - - Unit rider; - Unit soldier; - Unit ork; - - boolean showDecoration = Math.random()>0.5; - if (!showDecoration){ - rider = new Rider(); - soldier = new Soldier(); - ork = new Ork(); - }else{ - rider = new MagicDecorator(new Rider()); - soldier = new MagicDecorator(new Soldier()); - ork = new ColorDecorator( - new MagicDecorator(new Ork()) - ); - } - - rider.draw(); - soldier.draw(); - ork.draw(); - - - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/ColorDecorator.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/ColorDecorator.java deleted file mode 100644 index 95fc095..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/ColorDecorator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.decorators; - -import com.javarush.khmelov.lesson16.p05_decorator.objects.Unit; - -public class ColorDecorator extends Decorator { - public ColorDecorator(Unit component) { - super(component); - } - @Override - public void afterDraw() { - System.out.println(" ... added color"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/Decorator.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/Decorator.java deleted file mode 100644 index a3a262a..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/Decorator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.decorators; - -import com.javarush.khmelov.lesson16.p05_decorator.objects.Unit; - -abstract class Decorator implements Unit { - - private final Unit component; - - //ключевой элемент - конструктор с декорируемым элементом - Decorator (Unit component) { - this.component = component; - } - - //то что будет делать конкретный декоратор выносится в реализацию - public abstract void afterDraw(); - - //"довесок" к методу - @Override - public void draw() { - component.draw(); - afterDraw(); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/MagicDecorator.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/MagicDecorator.java deleted file mode 100644 index 663f08d..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/decorators/MagicDecorator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.decorators; - -import com.javarush.khmelov.lesson16.p05_decorator.objects.Unit; - -public class MagicDecorator extends Decorator { - public MagicDecorator(Unit component) { - super(component); - } - @Override - public void afterDraw() { - System.out.println("...added magic"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Ork.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Ork.java deleted file mode 100644 index f11dfde..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Ork.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.objects; - -public class Ork implements Unit { - - @Override - public void draw() { - System.out.println("draw Ork"); - } - - - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Rider.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Rider.java deleted file mode 100644 index 135246f..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Rider.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.objects; - -public class Rider implements Unit { - - @Override - public void draw() { - System.out.println("draw Rider"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Soldier.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Soldier.java deleted file mode 100644 index ad23991..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Soldier.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.objects; - - -public class Soldier implements Unit { - - @Override - public void draw() { - System.out.println("draw Soldier"); - } - - - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Unit.java b/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Unit.java deleted file mode 100644 index 59c71cd..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p05_decorator/objects/Unit.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javarush.khmelov.lesson16.p05_decorator.objects; - -public interface Unit { - - void draw(); - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PageListPrinter.java b/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PageListPrinter.java deleted file mode 100644 index c961e0b..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PageListPrinter.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javarush.khmelov.lesson16.p06_adapter.adapter; -import java.util.List; - -// интерфейс для печати списка текстов -interface PageListPrinter { - void print(List list); -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PrinterAdapter.java b/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PrinterAdapter.java deleted file mode 100644 index 88d9265..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/adapter/PrinterAdapter.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.javarush.khmelov.lesson16.p06_adapter.adapter; - -import com.javarush.khmelov.lesson16.p06_adapter.objects.Printer; - -import java.util.List; - -// адаптер, который совмещает желание клиента -// и возможности принтера -public class PrinterAdapter implements PageListPrinter{ - - private Printer printer = new Printer(); - - @Override - public void print(List list) { - for (String text : list) { - printer.print(text); - } - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Client.java b/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Client.java deleted file mode 100644 index 787ddc8..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Client.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.javarush.khmelov.lesson16.p06_adapter.objects; - -import com.javarush.khmelov.lesson16.p06_adapter.adapter.PrinterAdapter; - -import java.util.ArrayList; - -// клиент, который хотел бы уметь печатать сразу много текста -// работает с адаптером, а не с принтером напрямую -public class Client { - - public static void main(String[] args) { - ArrayList list = new ArrayList<>(); - list.add("text1"); - list.add("text2"); - list.add("text3"); - - - PrinterAdapter printerAdapter = new PrinterAdapter(); - printerAdapter.print(list); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Printer.java b/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Printer.java deleted file mode 100644 index 2bf78a1..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p06_adapter/objects/Printer.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson16.p06_adapter.objects; - -// готовый объект с возможностью печати 1 текста -public class Printer { - public void print(String text) { - System.out.println(text); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/facade/CarFacade.java b/src/main/java/com/javarush/khmelov/lesson16/p07_facade/facade/CarFacade.java deleted file mode 100644 index 9611046..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/facade/CarFacade.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.javarush.khmelov.lesson16.p07_facade.facade; - -// фасад для работы - -import com.javarush.khmelov.lesson16.p07_facade.parts.Clamping; -import com.javarush.khmelov.lesson16.p07_facade.parts.Door; -import com.javarush.khmelov.lesson16.p07_facade.parts.Wheel; - -public class CarFacade { - - private final Door door = new Door(); - private final Clamping clamping = new Clamping(); - private final Wheel wheel = new Wheel(); - - public void go(){ - door.open(); - clamping.fire(); - wheel.turn(); - } - - - -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/objects/Client.java b/src/main/java/com/javarush/khmelov/lesson16/p07_facade/objects/Client.java deleted file mode 100644 index 1707313..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/objects/Client.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.lesson16.p07_facade.objects; - -import com.javarush.khmelov.lesson16.p07_facade.facade.CarFacade; -import com.javarush.khmelov.lesson16.p07_facade.parts.Clamping; -import com.javarush.khmelov.lesson16.p07_facade.parts.Door; -import com.javarush.khmelov.lesson16.p07_facade.parts.Wheel; - -public class Client { - - public static void main(String[] args) { - // вызов без фасада - Door door = new Door(); - door.open(); - - Clamping clamping = new Clamping(); - clamping.fire(); - - Wheel wheel = new Wheel(); - wheel.turn(); - - - // вызов с фасадом - CarFacade carFacade = new CarFacade(); - carFacade.go(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Clamping.java b/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Clamping.java deleted file mode 100644 index d505c08..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Clamping.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson16.p07_facade.parts; - -public class Clamping { - - public void fire() { - System.out.println("fire"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Door.java b/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Door.java deleted file mode 100644 index c97a7d9..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Door.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javarush.khmelov.lesson16.p07_facade.parts; - -public class Door{ - public void open() { - System.out.println("door open"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Wheel.java b/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Wheel.java deleted file mode 100644 index 6b606f6..0000000 --- a/src/main/java/com/javarush/khmelov/lesson16/p07_facade/parts/Wheel.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javarush.khmelov.lesson16.p07_facade.parts; - -public class Wheel{ - public void turn() { - System.out.println("wheel turn"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Publisher.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Publisher.java deleted file mode 100644 index 05bb676..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Publisher.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; - -import java.util.ArrayList; - -class Publisher implements PublisherInterface { - private ArrayList listeners = new ArrayList<>(); - @Override - public ArrayList getListeners() { - return listeners; - } - @Override - public void addListener(Subscriber listener) { - listeners.add(listener); - } - @Override - public void removeListener(Subscriber listener) { - listeners.remove(listener); - } - @Override - public void removeAllListeners() { - listeners.clear(); - } - @Override - public void notifySubscribers(String message) { - for (Subscriber actionListener : listeners) actionListener.doAction(message); - } - void createNewMessage(String message) { - System.out.println("Publisher printed message " + message); - notifySubscribers(message); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/PublisherInterface.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/PublisherInterface.java deleted file mode 100644 index b3a3a00..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/PublisherInterface.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; -import java.util.ArrayList; - -interface PublisherInterface { - ArrayList getListeners(); - void addListener(Subscriber listener); - void removeListener(Subscriber listener); - void removeAllListeners(); - void notifySubscribers(String message); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Start.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Start.java deleted file mode 100644 index 75607a4..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Start.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; - -public class Start { - - public static void main(String[] args) { - Subscriber subscriber1 = new Subscriber1(); - Subscriber subscriber2 = new Subscriber2(); - Publisher publisher = new Publisher(); - publisher.addListener(subscriber1); - publisher.addListener(subscriber2); - publisher.createNewMessage("Message!"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber.java deleted file mode 100644 index 2e9549b..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; - -interface Subscriber { - void doAction(String message); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber1.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber1.java deleted file mode 100644 index 1a083a6..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber1.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; - -class Subscriber1 implements Subscriber { - @Override - public void doAction(String message) { - System.out.println(message + " from " - + this.getClass().getSimpleName()); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber2.java b/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber2.java deleted file mode 100644 index e7c565f..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p08_observer/Subscriber2.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson17.p08_observer; - -class Subscriber2 implements Subscriber { - @Override - public void doAction(String message) { - System.out.println(message + " from " - + this.getClass().getName()); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p09_state/context/TransformerContext.java b/src/main/java/com/javarush/khmelov/lesson17/p09_state/context/TransformerContext.java deleted file mode 100644 index 3983200..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p09_state/context/TransformerContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.javarush.khmelov.lesson17.p09_state.context; - - -import com.javarush.khmelov.lesson17.p09_state.state.TransformerState; - -public class TransformerContext implements TransformerState { - - private TransformerState state; - - public TransformerState getState() { - return state; - } - - public void setState(TransformerState state) { - this.state = state; - } - - @Override - public void action() { - this.state.action(); - } - - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p09_state/start/Main.java b/src/main/java/com/javarush/khmelov/lesson17/p09_state/start/Main.java deleted file mode 100644 index 7c57a60..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p09_state/start/Main.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.javarush.khmelov.lesson17.p09_state.start; - - -import com.javarush.khmelov.lesson17.p09_state.context.TransformerContext; -import com.javarush.khmelov.lesson17.p09_state.state.FireState; -import com.javarush.khmelov.lesson17.p09_state.state.MoveState; -import com.javarush.khmelov.lesson17.p09_state.state.TransformerState; - -public class Main { - - public static void main(String[] args) { - - TransformerContext context = new TransformerContext(); - - TransformerState stateMove = new MoveState(); - TransformerState stateFire = new FireState(); - - context.setState(stateFire); - context.action(); - - context.setState(stateMove); - context.action(); - - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/FireState.java b/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/FireState.java deleted file mode 100644 index c8f85bb..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/FireState.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javarush.khmelov.lesson17.p09_state.state; - -public class FireState implements TransformerState{ - - @Override - public void action() { - System.out.println("fire!!!"); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/MoveState.java b/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/MoveState.java deleted file mode 100644 index 9226ab9..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/MoveState.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.javarush.khmelov.lesson17.p09_state.state; - -public class MoveState implements TransformerState { - - @Override - public void action() { - System.out.println("move!!!"); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/TransformerState.java b/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/TransformerState.java deleted file mode 100644 index 1dcc689..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p09_state/state/TransformerState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.javarush.khmelov.lesson17.p09_state.state; - -public interface TransformerState { - - void action(); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/AuthStrategy.java b/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/AuthStrategy.java deleted file mode 100644 index 0273305..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/AuthStrategy.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.lesson17.p10_strategy; - -public interface AuthStrategy { - boolean checkLogin(String name, String password); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/DBAuth.java b/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/DBAuth.java deleted file mode 100644 index ebb0c56..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/DBAuth.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.javarush.khmelov.lesson17.p10_strategy; - -class DBAuth implements AuthStrategy{ - - private Object dbRef;// ссылка на базу данных - - DBAuth(String dbUrl) { - } - - private void createConnection(String dbUrl){ - // dbRef = .. - } - - - @Override - public boolean checkLogin(String name, String password) { - - System.out.println("Checking with DB..."); - - String userHash = getHash(name); - String passHash = getHash(password); - - return checkUser(userHash, passHash); - } - - - private boolean checkUser(String name, String password){ - - // проверка в базе данных через dbRef - - return true; - } - - private String getHash(String value){ - // хеширование - - return "2SDA23SD"; - } - - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/FileAuth.java b/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/FileAuth.java deleted file mode 100644 index c0649e7..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/FileAuth.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.lesson17.p10_strategy; - -import java.io.File; - -class FileAuth implements AuthStrategy{ - - private final File file; - - FileAuth(File file) { - this.file = file; - } - - - @Override - public boolean checkLogin(String name, String password) { - System.out.println("Checking with file..."); - return checkFromFile(); - } - - private boolean checkFromFile(){ - // считывание из файла данных - - return true; - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/Main.java b/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/Main.java deleted file mode 100644 index 3744faf..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/Main.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.javarush.khmelov.lesson17.p10_strategy; - -import java.io.File; - -public class Main { - - public static void main(String[] args) { - UserChecker userChecker = new UserChecker(); - userChecker.check(new DBAuth("jdbc://etc")); - userChecker.check(new FileAuth(new File("c:\\file.txt"))); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/UserChecker.java b/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/UserChecker.java deleted file mode 100644 index 92b5793..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p10_strategy/UserChecker.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.javarush.khmelov.lesson17.p10_strategy; - -public class UserChecker { - - private String name; - private String password; - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - - public void setName(String name) { - this.name = name; - } - - public void setPassword(String password) { - this.password = password; - } - - public boolean check(AuthStrategy authStrategy) { - return authStrategy.checkLogin(name, password); - } - -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Colleague.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Colleague.java deleted file mode 100644 index 2cba1bc..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Colleague.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - -abstract class Colleague { - - private final Mediator mediator; - - Colleague(Mediator mediator) { - this.mediator = mediator; - } - - void send(String message) { - mediator.send(message, this); - } - - public abstract void notify(String message); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague1.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague1.java deleted file mode 100644 index c726ad0..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague1.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - - class ConcreteColleague1 extends Colleague { - - ConcreteColleague1(Mediator mediator) { - super(mediator); - } - - @Override - public void notify(String message) { - System.out.println("Colleague1 gets message: " + message); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague2.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague2.java deleted file mode 100644 index 64452da..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteColleague2.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - -class ConcreteColleague2 extends Colleague { - - ConcreteColleague2(Mediator mediator) { - super(mediator); - } - - @Override - public void notify(String message) { - System.out.println("Colleague2 gets message: " + message); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteMediator.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteMediator.java deleted file mode 100644 index a75f607..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/ConcreteMediator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - -class ConcreteMediator extends Mediator { - - private ConcreteColleague1 colleague1; - private ConcreteColleague2 colleague2; - - void setColleague1(ConcreteColleague1 colleague) { - this.colleague1 = colleague; - } - - void setColleague2(ConcreteColleague2 colleague) { - this.colleague2 = colleague; - } - - @Override - public void send(String message, Colleague colleague) { - if (colleague.equals(colleague2)) { - colleague1.notify(message); - } else { - colleague2.notify(message); - } - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Main.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Main.java deleted file mode 100644 index 4a0b724..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Main.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - -public class Main { - - public static void main(String[] args) { - ConcreteMediator m = new ConcreteMediator(); - - ConcreteColleague1 c1 = new ConcreteColleague1(m); - ConcreteColleague2 c2 = new ConcreteColleague2(m); - - m.setColleague1(c1); - m.setColleague2(c2); - - c1.send("How are you?"); - c2.send("Fine, thanks"); - } -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Mediator.java b/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Mediator.java deleted file mode 100644 index d36d3b8..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p11_mediator/Mediator.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.lesson17.p11_mediator; - -abstract class Mediator { - public abstract void send(String message, Colleague colleague); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Dev.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Dev.java deleted file mode 100644 index 3fa2df1..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Dev.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -interface Dev { - void writeCode(); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevCpp.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevCpp.java deleted file mode 100644 index 8fcfb2c..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevCpp.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -class DevCpp implements Dev { - @Override - public void writeCode() { - System.out.println("CPP Developer write code..."); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevJava.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevJava.java deleted file mode 100644 index 0470ab5..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/DevJava.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -class DevJava implements Dev { - @Override - public void writeCode() { - System.out.println("JavaDeveloper write code..."); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Prg.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Prg.java deleted file mode 100644 index a8e6566..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Prg.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -abstract class Prg { - protected Dev developer; - - Prg(Dev developer) { - this.developer = developer; - } - - abstract void developProgram(); -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgBank.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgBank.java deleted file mode 100644 index b777056..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgBank.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -class PrgBank extends Prg { - PrgBank(Dev developer) { - super(developer); - } - - @Override - void developProgram() { - System.out.println("Bank dev in progress..."); - developer.writeCode(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgStock.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgStock.java deleted file mode 100644 index 3e8c0c8..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/PrgStock.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -class PrgStock extends Prg { - PrgStock(Dev developer) { - super(developer); - } - - @Override - void developProgram() { - System.out.println("Stock dev in progress..."); - developer.writeCode(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Runner.java b/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Runner.java deleted file mode 100644 index 5958461..0000000 --- a/src/main/java/com/javarush/khmelov/lesson17/p12_bridge/Runner.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.javarush.khmelov.lesson17.p12_bridge; - -public class Runner { - public static void main(String[] args) { - Prg[] prgs = { - new PrgBank(new DevJava()), - new PrgBank(new DevCpp()), - new PrgStock(new DevJava()) - }; - for (Prg prg : prgs) prg.developProgram(); - } -} diff --git a/src/main/java/com/javarush/khmelov/lesson18/PhantomReferenceExample.java b/src/main/java/com/javarush/khmelov/lesson18/PhantomReferenceExample.java deleted file mode 100644 index d19a124..0000000 --- a/src/main/java/com/javarush/khmelov/lesson18/PhantomReferenceExample.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.javarush.khmelov.lesson18; - -import java.lang.ref.PhantomReference; -import java.lang.ref.ReferenceQueue; -import java.time.LocalTime; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -public class PhantomReferenceExample { - public static void main(String[] args) throws Exception { - // Создаем очередь для обработки ссылок - var wait = new ReferenceQueue(); - // Создаем String и PhantomReference - String s = new String(new char[]{'H', 'i', '!'}); - var ref = new PhantomReference<>(s, wait); - System.out.println("Live: " + LocalTime.now()); - // Удаляем жесткую ссылку - s = null; - // Запускаем GC с паузой 1 сек - CompletableFuture - .delayedExecutor(1, TimeUnit.SECONDS) - .execute(System::gc); - //Ожидаем на lock-е, пока объект будет собран - wait.remove(); - System.out.println("Dead: " + LocalTime.now()); - } -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/lesson18/SoftReferenceExample.java b/src/main/java/com/javarush/khmelov/lesson18/SoftReferenceExample.java deleted file mode 100644 index e914034..0000000 --- a/src/main/java/com/javarush/khmelov/lesson18/SoftReferenceExample.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.javarush.khmelov.lesson18; - -import java.lang.ref.SoftReference; - -public class SoftReferenceExample { - public static void main(String[] args) { - String str = new String("Hello"); - SoftReference softRef = new SoftReference<>(str); - // мягкая ссылка на объект "Hello" - str = null; // Удаляем сильную ссылку - // В любой момент система может решить - // удалить объект из-за нехватки памяти - // Мы можем получить объект - // обратно из мягкой ссылки - var retrievedStr = softRef.get(); - if (retrievedStr != null) { - System.out.println(retrievedStr); - } else { - System.out.println("Object collected GC"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/lesson18/WeakReferenceExample.java b/src/main/java/com/javarush/khmelov/lesson18/WeakReferenceExample.java deleted file mode 100644 index 2c0cee6..0000000 --- a/src/main/java/com/javarush/khmelov/lesson18/WeakReferenceExample.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.khmelov.lesson18; - -import java.lang.ref.WeakReference; - -public class WeakReferenceExample { - public static void main(String[] args) { - var str = new String("Hello"); - var weakRef = new WeakReference<>(str); - //слабая ссылка на объект "Hello" - str = null; // Удаляем сильную ссылку на объект - // В любой момент система может решить - // удалить объект из-за отсутствия сильных ссылок - // можно попытаться получить его из слабой ссылки - var retrievedStr = weakRef.get(); - if (retrievedStr != null) { - System.out.println(retrievedStr); - } else { - System.out.println("Object collected GC"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/javarush/zyibin/config/AppInitListener.java b/src/main/java/com/javarush/zyibin/config/AppInitListener.java new file mode 100644 index 0000000..59a483b --- /dev/null +++ b/src/main/java/com/javarush/zyibin/config/AppInitListener.java @@ -0,0 +1,40 @@ +package com.javarush.zyibin.config; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.annotation.WebListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@WebListener +public class AppInitListener implements ServletContextListener { + private static final Logger log = LoggerFactory.getLogger(AppInitListener.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + log.info("Application context initialization started"); + + ApplicationConfig config = new ApplicationConfig(); + ServletContext context = sce.getServletContext(); + + + context.setAttribute("userRepository", config.getUserRepository()); + log.info("UserRepository initialized"); + + + context.setAttribute("testResultRepository", config.getTestResultRepository()); + log.info("TestResultRepository initialized"); + + + context.setAttribute("questionRepository", config.getQuestionRepository()); + log.info("QuestionRepository initialized"); + + log.info("Application context initialization completed successfully"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + log.info("Application context destroyed"); + } +} diff --git a/src/main/java/com/javarush/zyibin/config/ApplicationConfig.java b/src/main/java/com/javarush/zyibin/config/ApplicationConfig.java new file mode 100644 index 0000000..781be2d --- /dev/null +++ b/src/main/java/com/javarush/zyibin/config/ApplicationConfig.java @@ -0,0 +1,27 @@ +package com.javarush.zyibin.config; + +import com.javarush.zyibin.repository.*; + +public class ApplicationConfig { + private final UserRepository userRepository; + private final TestResultRepository testResultRepository; + private final QuestionRepository questionRepository; + + public ApplicationConfig() { + this.userRepository = new InMemoryUserRepository(); + this.testResultRepository = new InMemoryTestResultRepository(); + this.questionRepository = QuestionRepository.defaultRepository(); + } + + public UserRepository getUserRepository() { + return userRepository; + } + + public TestResultRepository getTestResultRepository() { + return testResultRepository; + } + + public QuestionRepository getQuestionRepository() { + return questionRepository; + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/AvatarSelectServlet.java b/src/main/java/com/javarush/zyibin/controllers/AvatarSelectServlet.java new file mode 100644 index 0000000..55ddedc --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/AvatarSelectServlet.java @@ -0,0 +1,54 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.service.AvatarService; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet("/profile/avatar") +public class AvatarSelectServlet extends BaseServlet { + + private final AvatarService avatarService = new AvatarService(); + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /profile/avatar"); + + List avatars = avatarService.getAvailableAvatars(); + log.debug("Loaded {} available avatars", avatars.size()); + + req.setAttribute("avatars", avatars); + req.getRequestDispatcher("/WEB-INF/jsp/avatar-select.jsp").forward(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /profile/avatar"); + + User user = getCurrentUser(req); + String selectedAvatar = req.getParameter("avatarPath"); + log.debug("Selected avatar path: {}", selectedAvatar); + + List availableAvatars = avatarService.getAvailableAvatars(); + if (availableAvatars.contains(selectedAvatar)) { + log.info("User {} changed avatar to {}", user.getUsername(), selectedAvatar); + user.setAvatarPath(selectedAvatar); + } else { + log.warn("User {} attempted to select invalid avatar: {}", + user.getUsername(), + selectedAvatar); + } + resp.sendRedirect(req.getContextPath() + "/profile"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/AvatarUploadServlet.java b/src/main/java/com/javarush/zyibin/controllers/AvatarUploadServlet.java new file mode 100644 index 0000000..066dc40 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/AvatarUploadServlet.java @@ -0,0 +1,61 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.UUID; + +@WebServlet("/profile/avatar/upload") +@MultipartConfig( + fileSizeThreshold = 1024 * 1024, // 1MB + maxFileSize = 5 * 1024 * 1024, // 5MB + maxRequestSize = 6 * 1024 * 1024 // 6MB +) +public class AvatarUploadServlet extends BaseServlet { + + private static final String UPLOAD_DIR = "/uploads/avatars"; + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /profile/avatar/upload"); + + User user = getCurrentUser(req); + log.debug("User {} initiates avatar upload", user.getUsername()); + + Part filePart = req.getPart("avatar"); + if (filePart == null || filePart.getSize() == 0) { + log.warn("Пользователь {} попытался загрузить пустой файл", user.getUsername()); + resp.sendRedirect(req.getContextPath() + "/profile/avatar"); + return; + } + + String submittedFileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); + String extension = submittedFileName.substring(submittedFileName.lastIndexOf('.')); + String fileName = UUID.randomUUID() + extension; + String uploadPath = getServletContext().getRealPath(UPLOAD_DIR); + File uploadDir = new File(uploadPath); + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + + File file = new File(uploadDir, fileName); + filePart.write(file.getAbsolutePath()); + + user.setAvatarPath(UPLOAD_DIR + "/" + fileName); + log.info("User {} successfully uploaded avatar: {}", user.getUsername(), fileName); + resp.sendRedirect(req.getContextPath() + "/profile"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/BaseServlet.java b/src/main/java/com/javarush/zyibin/controllers/BaseServlet.java new file mode 100644 index 0000000..38026c1 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/BaseServlet.java @@ -0,0 +1,101 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.handler.RequestHandler; +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.QuestionRepository; +import com.javarush.zyibin.repository.TestResultRepository; +import com.javarush.zyibin.repository.UserRepository; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class BaseServlet extends HttpServlet { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected UserRepository userRepository; + protected TestResultRepository testResultRepository; + protected QuestionRepository questionRepository; + protected RequestHandler requestHandler; + + @Override + public void init() throws ServletException { + super.init(); + initializeCommonDependencies(); + initializeSpecificServices(); + log.debug("{} initialized successfully", getClass().getSimpleName()); + } + + private void initializeCommonDependencies() throws ServletException { + ServletContext context = getServletContext(); + + this.userRepository = (UserRepository) context.getAttribute("userRepository"); + this.testResultRepository = (TestResultRepository) context.getAttribute("testResultRepository"); + this.questionRepository = (QuestionRepository) context.getAttribute("questionRepository"); + this.requestHandler = new RequestHandler(); + + validateDependencies(); + } + + private void validateDependencies() throws ServletException { + if (userRepository == null) { + throw new ServletException("UserRepository not found in ServletContext"); + } + if (testResultRepository == null) { + throw new ServletException("TestResultRepository not found in ServletContext"); + } + if (questionRepository == null) { + throw new ServletException("QuestionRepository not found in ServletContext"); + } + if (requestHandler == null) { + throw new ServletException("RequestHandler initialization failed"); + } + } + + protected abstract void initializeSpecificServices(); + + protected User getCurrentUser(HttpServletRequest req) { + HttpSession session = req.getSession(false); + if (session == null) { + return null; + } + return (User) session.getAttribute("currentUser"); + } + + protected boolean isUserAuthenticated(HttpServletRequest req) { + return getCurrentUser(req) != null; + } + + protected boolean isCurrentUserAdmin(HttpServletRequest req) { + User user = getCurrentUser(req); + return user != null && user.getRole() == Role.ADMIN; + } + + protected void setCurrentUser(HttpServletRequest req, User user) { + HttpSession session = req.getSession(true); + session.setAttribute("currentUser", user); + log.debug("User {} set in session", user.getUsername()); + } + + protected void clearCurrentUser(HttpServletRequest req) { + HttpSession session = req.getSession(false); + if (session != null) { + User user = getCurrentUser(req); + session.invalidate(); + if (user != null) { + log.debug("User {} removed from session", user.getUsername()); + } + } + } + + @Override + public void destroy() { + log.debug("{} destroyed", getClass().getSimpleName()); + super.destroy(); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/HomeServlet.java b/src/main/java/com/javarush/zyibin/controllers/HomeServlet.java new file mode 100644 index 0000000..503ddda --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/HomeServlet.java @@ -0,0 +1,23 @@ +package com.javarush.zyibin.controllers; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/home") +public class HomeServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /home"); + req.getRequestDispatcher("/WEB-INF/jsp/home.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/LoginServlet.java b/src/main/java/com/javarush/zyibin/controllers/LoginServlet.java new file mode 100644 index 0000000..02ef194 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/LoginServlet.java @@ -0,0 +1,45 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.service.AuthenticationService; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/login") +public class LoginServlet extends BaseServlet { + + private AuthenticationService authService; + + @Override + protected void initializeSpecificServices() { + this.authService = new AuthenticationService(userRepository); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /login"); + req.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String username = req.getParameter("username"); + String password = req.getParameter("password"); + + requestHandler.handleRequest(req, resp, () -> { + User user = authService.authenticate(username, password); + + setCurrentUser(req, user); + + log.info("User {} successfully logged in", username); + resp.sendRedirect(req.getContextPath() + "/home"); + }, "/WEB-INF/jsp/login.jsp"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/LogoutServlet.java b/src/main/java/com/javarush/zyibin/controllers/LogoutServlet.java new file mode 100644 index 0000000..55c6c7b --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/LogoutServlet.java @@ -0,0 +1,40 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/logout") +public class LogoutServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + performLogout(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + performLogout(req, resp); + } + + private void performLogout(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + User currentUser = getCurrentUser(req); + if (currentUser != null) { + log.info("User {} logged out", currentUser.getUsername()); + } + + clearCurrentUser(req); + resp.sendRedirect(req.getContextPath() + "/login"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/ProfileEditServer.java b/src/main/java/com/javarush/zyibin/controllers/ProfileEditServer.java new file mode 100644 index 0000000..36e5c2d --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/ProfileEditServer.java @@ -0,0 +1,40 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/profile/edit") +public class ProfileEditServer extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /profile/edit"); + req.getRequestDispatcher("/WEB-INF/jsp/profile-edit.jsp").forward(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /profile/edit"); + + User user = getCurrentUser(req); + String nickname = req.getParameter("nickname"); + String about = req.getParameter("about"); + + user.setNickname(nickname); + user.setAbout(about); + log.info("User {} updated profile data", user.getUsername()); + + resp.sendRedirect(req.getContextPath() + "/profile"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/ProfileServlet.java b/src/main/java/com/javarush/zyibin/controllers/ProfileServlet.java new file mode 100644 index 0000000..8c30176 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/ProfileServlet.java @@ -0,0 +1,45 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.TestResult; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.service.UserStatisticsService; +import com.javarush.zyibin.service.UserStatisticsServiceImpl; +import com.javarush.zyibin.service.UserTestStatisticsService; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet("/profile") +public class ProfileServlet extends BaseServlet { + + private UserStatisticsService topicStatisticsService; + private UserTestStatisticsService testStatisticsService; + + @Override + protected void initializeSpecificServices() { + this.topicStatisticsService = new UserStatisticsServiceImpl(); + this.testStatisticsService = new UserTestStatisticsService(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /profile"); + + User user = getCurrentUser(req); + log.debug("Loading test results for user {}", user.getUsername()); + + List results = testResultRepository.findByUserId(user.getId()); + req.setAttribute("results", results); + + req.setAttribute("testStats", testStatisticsService.calculate(results)); + req.setAttribute("topicStats", topicStatisticsService.calculateUserTopicStats(results)); + + log.info("Profile page prepared for user {}", user.getUsername()); + req.getRequestDispatcher("/WEB-INF/jsp/profile.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/QuestionServlet.java b/src/main/java/com/javarush/zyibin/controllers/QuestionServlet.java new file mode 100644 index 0000000..e71fb84 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/QuestionServlet.java @@ -0,0 +1,73 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.InterviewState; +import com.javarush.zyibin.service.QuestionService; +import com.javarush.zyibin.session.SessionUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet("/question") +public class QuestionServlet extends BaseServlet { + + private QuestionService questionService; + + @Override + protected void initializeSpecificServices() { + this.questionService = new QuestionService(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + HttpSession session = req.getSession(false); + + if (!SessionUtils.hasInterview(session)) { + log.debug("No active interview in session, redirecting to /start"); + resp.sendRedirect(req.getContextPath() + "/start"); + return; + } + + InterviewState state = SessionUtils.getInterviewState(session); + + if (state.isFinished()) { + log.info("Interview finished, redirecting to /result"); + resp.sendRedirect(req.getContextPath() + "/result"); + return; + } + + log.debug("Displaying question {} of {}", state.getCurrentIndex(), state.getTotalQuestions()); + + req.setAttribute("topics", state.getTopics()); + req.setAttribute("question", state.getCurrentQuestion()); + req.setAttribute("questionNumber", state.getCurrentIndex() + 1); + req.setAttribute("totalQuestions", state.getTotalQuestions()); + + req.getRequestDispatcher("/WEB-INF/jsp/question.jsp").forward(req, resp); + } + + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + HttpSession session = req.getSession(false); + + if (!SessionUtils.hasInterview(session)) { + log.debug("No active interview in session, redirecting to /start"); + resp.sendRedirect(req.getContextPath() + "/start"); + return; + } + + String answerIndexParam = req.getParameter("answerIndex"); + + requestHandler.handleRequest(req, resp, () -> { + InterviewState state = SessionUtils.getInterviewState(session); + questionService.processAnswer(state, answerIndexParam); + resp.sendRedirect(req.getContextPath() + "/question"); + }, "/WEB-INF/jsp/question.jsp"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/RegistrationServlet.java b/src/main/java/com/javarush/zyibin/controllers/RegistrationServlet.java new file mode 100644 index 0000000..a9c913f --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/RegistrationServlet.java @@ -0,0 +1,46 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.service.RegistrationService; +import com.javarush.zyibin.service.UserService; +import com.javarush.zyibin.service.UserServiceImpl; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/register") +public class RegistrationServlet extends BaseServlet { + + private RegistrationService registrationService; + + @Override + protected void initializeSpecificServices() { + UserService userService = new UserServiceImpl(userRepository); + this.registrationService = new RegistrationService(userService); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /register"); + req.getRequestDispatcher("/WEB-INF/jsp/register.jsp").forward(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String username = req.getParameter("username"); + String password = req.getParameter("password"); + String email = req.getParameter("email"); + + requestHandler.handleRequest(req, resp, () -> { + User user = registrationService.registerUser(username, password, email); + log.info("User {} successfully registered", user.getUsername()); + resp.sendRedirect(req.getContextPath() + "/login"); + }, "/WEB-INF/jsp/register.jsp"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/ResultServlet.java b/src/main/java/com/javarush/zyibin/controllers/ResultServlet.java new file mode 100644 index 0000000..b4c9686 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/ResultServlet.java @@ -0,0 +1,83 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.InterviewState; +import com.javarush.zyibin.model.TestResult; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.stream.Collectors; + +@WebServlet("/result") +public class ResultServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /result"); + + HttpSession session = req.getSession(false); + + if (session == null) { + log.debug("No session found, redirecting to /home"); + resp.sendRedirect(req.getContextPath() + "/home"); + return; + } + + User user = getCurrentUser(req); + + InterviewState interviewState = (InterviewState) session.getAttribute("interviewState"); + if (interviewState == null) { + log.debug("No interview state found, redirecting to /home"); + resp.sendRedirect(req.getContextPath() + "/home"); + return; + } + + int totalQuestions = interviewState.getTotalQuestions(); + int correctAnswers = interviewState.getScore(); + boolean passed = correctAnswers * 2 >= totalQuestions; + + String topicCodes = interviewState.getTopics() + .stream() + .map(Topic::getCode) + .collect(Collectors.joining(", ")); + + log.info("Interview finished for user {}, passed={}, score={}/{}", + user.getUsername(), + passed, + correctAnswers, + totalQuestions); + + TestResult result = new TestResult( + user.getId(), + topicCodes, + totalQuestions, + correctAnswers, + passed, + LocalDateTime.now() + ); + + testResultRepository.save(result); + + log.info("Test result saved for user {}", user.getUsername()); + + req.setAttribute("topics", topicCodes); + req.setAttribute("total", totalQuestions); + req.setAttribute("correct", correctAnswers); + req.setAttribute("passed", passed); + + session.removeAttribute("interviewState"); + + req.getRequestDispatcher("/WEB-INF/jsp/result.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/StartServlet.java b/src/main/java/com/javarush/zyibin/controllers/StartServlet.java new file mode 100644 index 0000000..c865185 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/StartServlet.java @@ -0,0 +1,69 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.session.SessionUtils; +import com.javarush.zyibin.model.InterviewState; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +@WebServlet("/start") +public class StartServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /start"); + + String[] topicParams = req.getParameterValues("topics"); + if (topicParams == null || topicParams.length == 0) { + log.warn("Interview start failed: no topics selected"); + throw new IllegalArgumentException("No topics selected"); + } + + Set selectedTopics = Arrays.stream(topicParams) + .map(Topic::valueOf) + .collect(Collectors.toSet()); + + log.debug("Selected topics: {}", selectedTopics.stream() + .map(Topic::getCode) + .collect(Collectors.joining(", "))); + + int questionCount = Integer.parseInt(req.getParameter("questionCount")); + + List allQuestions = new ArrayList<>(); + for (Topic topic : selectedTopics) { + allQuestions.addAll(questionRepository.getQuestions(topic)); + } + + if (allQuestions.size() < questionCount) { + log.warn("Not enough questions: requested={}, available={}", + questionCount, allQuestions.size()); + throw new IllegalStateException("Not enough questions for the topic"); + } + + Collections.shuffle(allQuestions); + List selectedQuestions = allQuestions.subList(0, questionCount); + + InterviewState interviewState = new InterviewState(selectedTopics, selectedQuestions); + HttpSession session = req.getSession(true); + SessionUtils.setInterviewState(session, interviewState); + + log.info("Interview started: topics={}, questions={}", + selectedTopics.size(), questionCount); + + resp.sendRedirect(req.getContextPath() + "/question"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/javarush/zyibin/controllers/TestSettingServlet.java b/src/main/java/com/javarush/zyibin/controllers/TestSettingServlet.java new file mode 100644 index 0000000..24d87f4 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/TestSettingServlet.java @@ -0,0 +1,24 @@ +package com.javarush.zyibin.controllers; + +import com.javarush.zyibin.model.Topic; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/test/settings") +public class TestSettingServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + log.debug("GET /test/settings"); + req.setAttribute("topics", Topic.values()); + req.getRequestDispatcher("/WEB-INF/jsp/test-settings.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/admin/AdminBlockUserServlet.java b/src/main/java/com/javarush/zyibin/controllers/admin/AdminBlockUserServlet.java new file mode 100644 index 0000000..ff4a466 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/admin/AdminBlockUserServlet.java @@ -0,0 +1,39 @@ +package com.javarush.zyibin.controllers.admin; + +import com.javarush.zyibin.controllers.BaseServlet; +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/admin/users/block") +public class AdminBlockUserServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /admin/users/block"); + + User admin = getCurrentUser(req); + long userId = Long.parseLong(req.getParameter("userId")); + + userRepository.findById(userId).ifPresent(user -> { + if (user.getId() != admin.getId()) { + user.setBlocked(!user.isBlocked()); + log.info("Admin {} changed block status for user {} to {}", + admin.getUsername(), + user.getUsername(), + user.isBlocked() ? "blocked" : "unblocked"); + } + }); + + resp.sendRedirect(req.getContextPath() + "/admin/users"); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/admin/AdminChangeRoleServlet.java b/src/main/java/com/javarush/zyibin/controllers/admin/AdminChangeRoleServlet.java new file mode 100644 index 0000000..0152c45 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/admin/AdminChangeRoleServlet.java @@ -0,0 +1,42 @@ +package com.javarush.zyibin.controllers.admin; + +import com.javarush.zyibin.controllers.BaseServlet; +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.service.AdminUserService; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/admin/users/role") +public class AdminChangeRoleServlet extends BaseServlet { + + private AdminUserService adminService; + + @Override + protected void initializeSpecificServices() { + this.adminService = new AdminUserService(userRepository); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("POST /admin/users/role"); + + User admin = getCurrentUser(req); + long userId = Long.parseLong(req.getParameter("userId")); + Role newRole = Role.valueOf(req.getParameter("role")); + + log.info("Admin {} requested role change for userId={} to role={}", + admin.getUsername(), + userId, + newRole); + + adminService.changeUserRole(admin.getId(), userId, newRole); + resp.sendRedirect(req.getContextPath() + "/admin/users"); + } +} + diff --git a/src/main/java/com/javarush/zyibin/controllers/admin/AdminServlet.java b/src/main/java/com/javarush/zyibin/controllers/admin/AdminServlet.java new file mode 100644 index 0000000..a85b672 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/admin/AdminServlet.java @@ -0,0 +1,29 @@ +package com.javarush.zyibin.controllers.admin; + +import com.javarush.zyibin.controllers.BaseServlet; +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/admin") +public class AdminServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /admin"); + + User user = getCurrentUser(req); + log.debug("Admin panel accessed by user {}", user.getUsername()); + + req.getRequestDispatcher("/WEB-INF/jsp/admin/admin.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/admin/AdminStatisticsServlet.java b/src/main/java/com/javarush/zyibin/controllers/admin/AdminStatisticsServlet.java new file mode 100644 index 0000000..1dfac8c --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/admin/AdminStatisticsServlet.java @@ -0,0 +1,84 @@ +package com.javarush.zyibin.controllers.admin; + +import com.javarush.zyibin.controllers.BaseServlet; +import com.javarush.zyibin.dto.TopicStats; +import com.javarush.zyibin.dto.UserStats; +import com.javarush.zyibin.model.TestResult; +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@WebServlet("/admin/statistics") +public class AdminStatisticsServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /admin/statistics"); + + List results = testResultRepository.findAll(); + List users = userRepository.findAll(); + + int totalTests = results.size(); + long passedTests = results.stream() + .filter(TestResult::isPassed) + .count(); + + Map userStats = new HashMap<>(); + for (User user : users) { + userStats.put(user.getId(), new UserStats(user)); + } + + for (TestResult r : results) { + UserStats stats = userStats.get(r.getUserId()); + if (stats != null) { + stats.incrementTotal(); + if (r.isPassed()) { + stats.incrementPassed(); + } + } + } + + Map topicStats = new HashMap<>(); + + for (TestResult r : results) { + String[] topics = r.getTopicCode().split(","); + + for (String rawTopic : topics) { + String topic = rawTopic.trim(); + + TopicStats stats = + topicStats.computeIfAbsent(topic, TopicStats::new); + + stats.incrementTotal(); + if (r.isPassed()) { + stats.incrementPassed(); + } + } + } + + log.info("Admin statistics prepared: totalTests={}, passedTests={}, users={}", + totalTests, + passedTests, + users.size()); + + req.setAttribute("totalTests", totalTests); + req.setAttribute("passedTests", passedTests); + req.setAttribute("userStats", userStats.values()); + req.setAttribute("topicStats", topicStats.values()); + + req.getRequestDispatcher("/WEB-INF/jsp/admin/statistics.jsp") + .forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/controllers/admin/AdminUserServlet.java b/src/main/java/com/javarush/zyibin/controllers/admin/AdminUserServlet.java new file mode 100644 index 0000000..4023997 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/controllers/admin/AdminUserServlet.java @@ -0,0 +1,33 @@ +package com.javarush.zyibin.controllers.admin; + +import com.javarush.zyibin.controllers.BaseServlet; +import com.javarush.zyibin.model.User; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet("/admin/users") +public class AdminUserServlet extends BaseServlet { + + @Override + protected void initializeSpecificServices() { + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + log.debug("GET /admin/users"); + + User admin = getCurrentUser(req); + List users = userRepository.findAll(); + + log.info("Admin {} loaded users list, count={}", admin.getUsername(), users.size()); + + req.setAttribute("users", users); + req.getRequestDispatcher("/WEB-INF/jsp/admin/users.jsp").forward(req, resp); + } +} diff --git a/src/main/java/com/javarush/zyibin/dto/BaseStats.java b/src/main/java/com/javarush/zyibin/dto/BaseStats.java new file mode 100644 index 0000000..6f7c7ae --- /dev/null +++ b/src/main/java/com/javarush/zyibin/dto/BaseStats.java @@ -0,0 +1,29 @@ +package com.javarush.zyibin.dto; + +public abstract class BaseStats { + protected int total; + protected int passed; + + public void incrementTotal() { + total++; + } + + public void incrementPassed() { + passed++; + } + + public int getTotal() { + return total; + } + + public int getPassed() { + return passed; + } + + public int getSuccessRate() { + if (total == 0) { + return 0; + } + return (passed * 100) / total; + } +} diff --git a/src/main/java/com/javarush/zyibin/dto/TopicStats.java b/src/main/java/com/javarush/zyibin/dto/TopicStats.java new file mode 100644 index 0000000..63ee036 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/dto/TopicStats.java @@ -0,0 +1,25 @@ +package com.javarush.zyibin.dto; + +import com.javarush.zyibin.model.Topic; + +public class TopicStats extends BaseStats { + + private final String topicCode; + private String topicDisplayName; + + + public TopicStats(String topicCode) { + this.topicCode = topicCode; + Topic topic = Topic.fromCode(topicCode); + this.topicDisplayName = topic.getDisplayName(); + } + + public String getTopicCode() { + return topicCode; + } + + public String getTopicDisplayName() { + return topicDisplayName; + } + +} diff --git a/src/main/java/com/javarush/zyibin/dto/UserStats.java b/src/main/java/com/javarush/zyibin/dto/UserStats.java new file mode 100644 index 0000000..9062bbe --- /dev/null +++ b/src/main/java/com/javarush/zyibin/dto/UserStats.java @@ -0,0 +1,17 @@ +package com.javarush.zyibin.dto; + +import com.javarush.zyibin.model.User; + +public class UserStats extends BaseStats { + + private final String username; + + public UserStats(User user) { + this.username = user.getUsername(); + } + + public String getUsername() { + return username; + } + +} diff --git a/src/main/java/com/javarush/zyibin/dto/UserTestStats.java b/src/main/java/com/javarush/zyibin/dto/UserTestStats.java new file mode 100644 index 0000000..ce4fe8b --- /dev/null +++ b/src/main/java/com/javarush/zyibin/dto/UserTestStats.java @@ -0,0 +1,15 @@ +package com.javarush.zyibin.dto; + +public class UserTestStats extends BaseStats { + + private final String testName; + + public UserTestStats(String testName) { + this.testName = testName; + } + + public String getTestName() { + return testName; + } + +} diff --git a/src/main/java/com/javarush/zyibin/dto/UserTopicStats.java b/src/main/java/com/javarush/zyibin/dto/UserTopicStats.java new file mode 100644 index 0000000..a6796a6 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/dto/UserTopicStats.java @@ -0,0 +1,15 @@ +package com.javarush.zyibin.dto; + +public class UserTopicStats extends BaseStats { + + private final String topicDisplayName; + + public UserTopicStats(String topicDisplayName) { + this.topicDisplayName = topicDisplayName; + } + + public String getTopicDisplayName() { + return topicDisplayName; + } + +} diff --git a/src/main/java/com/javarush/zyibin/exception/AuthenticationException.java b/src/main/java/com/javarush/zyibin/exception/AuthenticationException.java new file mode 100644 index 0000000..b07bc9b --- /dev/null +++ b/src/main/java/com/javarush/zyibin/exception/AuthenticationException.java @@ -0,0 +1,27 @@ +package com.javarush.zyibin.exception; + +public class AuthenticationException extends RuntimeException { + + private final String reason; + + public AuthenticationException(String message, String reason) { + super(message); + this.reason = reason; + } + + public String getReason() { + return reason; + } + + public static AuthenticationException invalidCredentials() { + return new AuthenticationException("Invalid login or password", "INVALID_CREDENTIALS"); + } + + public static AuthenticationException userBlocked() { + return new AuthenticationException("User is blocked", "USER_BLOCKED"); + } + + public static AuthenticationException userNotFound() { + return new AuthenticationException("User not found", "USER_NOT_FOUND"); + } +} diff --git a/src/main/java/com/javarush/zyibin/exception/ValidationException.java b/src/main/java/com/javarush/zyibin/exception/ValidationException.java new file mode 100644 index 0000000..c4813ba --- /dev/null +++ b/src/main/java/com/javarush/zyibin/exception/ValidationException.java @@ -0,0 +1,43 @@ +package com.javarush.zyibin.exception; + +public class ValidationException extends RuntimeException { + + private final String field; + private final String errorCode; + + public ValidationException(String message, String field) { + super(message); + this.field = field; + this.errorCode = "VALIDATION_ERROR"; + } + + public ValidationException(String message, String field, String errorCode) { + super(message); + this.field = field; + this.errorCode = errorCode; + } + + public String getField() { + return field; + } + + public String getErrorCode() { + return errorCode; + } + + public static ValidationException username(String message) { + return new ValidationException(message, "username", "USERNAME_INVALID"); + } + + public static ValidationException password(String message) { + return new ValidationException(message, "password", "PASSWORD_INVALID"); + } + + public static ValidationException email(String message) { + return new ValidationException(message, "email", "EMAIL_INVALID"); + } + + public static ValidationException general(String message) { + return new ValidationException(message, "general", "GENERAL_ERROR"); + } +} diff --git a/src/main/java/com/javarush/zyibin/filter/AdminFilter.java b/src/main/java/com/javarush/zyibin/filter/AdminFilter.java new file mode 100644 index 0000000..7b0958d --- /dev/null +++ b/src/main/java/com/javarush/zyibin/filter/AdminFilter.java @@ -0,0 +1,45 @@ +package com.javarush.zyibin.filter; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +@WebFilter("/admin/*") +public class AdminFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger(AdminFilter.class); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) servletRequest; + HttpServletResponse resp = (HttpServletResponse) servletResponse; + String path = req.getRequestURI(); + log.debug("Admin access attempt: {}", path); + + HttpSession session = req.getSession(false); + + User user = (User) session.getAttribute("currentUser"); + + if (user == null || user.getRole() != Role.ADMIN) { + log.warn("Admin access denied. user={}, path={}", + user != null ? user.getUsername() : "anonymous", + path + ); + resp.sendRedirect(req.getContextPath() + "/home"); + return; + } + log.debug("Admin access granted. user={}, path={}", + user.getUsername(), + path); + + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/src/main/java/com/javarush/zyibin/filter/AuthFilter.java b/src/main/java/com/javarush/zyibin/filter/AuthFilter.java new file mode 100644 index 0000000..82170ac --- /dev/null +++ b/src/main/java/com/javarush/zyibin/filter/AuthFilter.java @@ -0,0 +1,43 @@ +package com.javarush.zyibin.filter; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +@WebFilter(urlPatterns = { + "/profile/*", + "/test/*", + "/admin/*", + "/question/*", + "/result" +}) +public class AuthFilter implements Filter { + + private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) servletRequest; + HttpServletResponse resp = (HttpServletResponse) servletResponse; + String path = req.getRequestURI(); + log.debug("Auth check for path={}", path); + + HttpSession session = req.getSession(false); + + if (session == null || session.getAttribute("currentUser") == null) { + log.warn( + "Unauthorized access attempt, redirecting to login. path={}", path); + resp.sendRedirect(req.getContextPath() + "/login"); + return; + } + + log.debug("Auth check passed for path={}", path); + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/src/main/java/com/javarush/zyibin/handler/ErrorHandler.java b/src/main/java/com/javarush/zyibin/handler/ErrorHandler.java new file mode 100644 index 0000000..8d7a314 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/handler/ErrorHandler.java @@ -0,0 +1,96 @@ +package com.javarush.zyibin.handler; + +import com.javarush.zyibin.exception.AuthenticationException; +import com.javarush.zyibin.exception.ValidationException; +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; + +public class ErrorHandler { + private final Logger log = LoggerFactory.getLogger(ErrorHandler.class); + + + public void handleValidationError(HttpServletRequest req, HttpServletResponse resp, + ValidationException e, String returnPage) throws IOException { + log.warn("Validation error: field={}, message={}", e.getField(), e.getMessage()); + + req.setAttribute("error", e.getMessage()); + req.setAttribute("errorField", e.getField()); + req.setAttribute("errorCode", e.getErrorCode()); + + preserveFormData(req); + + try { + req.getRequestDispatcher(returnPage).forward(req, resp); + } catch (Exception ex) { + log.error("Error forwarding to error page", ex); + resp.sendRedirect(req.getContextPath() + "/error"); + } + } + + + public void handleAuthenticationError(HttpServletRequest req, HttpServletResponse resp, + AuthenticationException e) throws IOException { + log.warn("Authentication error: reason={}, message={}", e.getReason(), e.getMessage()); + + String errorMessage = getAuthenticationErrorMessage(e); + req.setAttribute("error", errorMessage); + + preserveFormData(req); + + try { + req.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(req, resp); + } catch (Exception ex) { + log.error("Error forwarding to login page", ex); + resp.sendRedirect(req.getContextPath() + "/login"); + } + } + + + public void handleGeneralError(HttpServletRequest req, HttpServletResponse resp, + Exception e) throws IOException { + log.error("General error occurred", e); + + req.setAttribute("error", "An unexpected error occurred. Please try again later."); + req.setAttribute("errorCode", "INTERNAL_ERROR"); + + try { + req.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(req, resp); + } catch (Exception ex) { + log.error("Error forwarding to error page", ex); + resp.sendRedirect(req.getContextPath() + "/error"); + } + } + + + private void preserveFormData(HttpServletRequest req) { + Map formData = new HashMap<>(); + + req.getParameterMap().forEach((key, values) -> { + if (values != null && values.length > 0) { + formData.put(key, values[0]); + } + }); + + req.setAttribute("formData", formData); + } + + + private String getAuthenticationErrorMessage(AuthenticationException e) { + switch (e.getReason()) { + case "INVALID_CREDENTIALS": + return "Invalid login or password"; + case "USER_BLOCKED": + return "User blocked by administrator"; + case "USER_NOT_FOUND": + return "User not found"; + default: + return e.getMessage(); + } + } +} diff --git a/src/main/java/com/javarush/zyibin/handler/RequestHandler.java b/src/main/java/com/javarush/zyibin/handler/RequestHandler.java new file mode 100644 index 0000000..bb900c9 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/handler/RequestHandler.java @@ -0,0 +1,36 @@ +package com.javarush.zyibin.handler; + +import com.javarush.zyibin.exception.AuthenticationException; +import com.javarush.zyibin.exception.ValidationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + + +public class RequestHandler { + + private final ErrorHandler errorHandler; + + public RequestHandler() { + this.errorHandler = new ErrorHandler(); + } + + public void handleRequest(HttpServletRequest request, HttpServletResponse response, + RequestAction action, String errorPage) throws IOException { + try { + action.execute(); + } catch (ValidationException e) { + errorHandler.handleValidationError(request, response, e, errorPage); + } catch (AuthenticationException e) { + errorHandler.handleAuthenticationError(request, response, e); + } catch (Exception e) { + errorHandler.handleGeneralError(request, response, e); + } + } + + @FunctionalInterface + public interface RequestAction { + void execute() throws Exception; + } +} diff --git a/src/main/java/com/javarush/zyibin/model/InterviewState.java b/src/main/java/com/javarush/zyibin/model/InterviewState.java new file mode 100644 index 0000000..51a9682 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/InterviewState.java @@ -0,0 +1,50 @@ +package com.javarush.zyibin.model; + +import java.util.List; +import java.util.Set; + +public class InterviewState { + private final Set topics; + private final List questions; + private int currentIndex; + private int score; + + public InterviewState(Set topics, List questions) { + this.topics = topics; + this.questions = questions; + this.currentIndex = 0; + this.score = 0; + } + + public Question getCurrentQuestion() { + return questions.get(currentIndex); + } + + public boolean isFinished() { + return currentIndex >= questions.size(); + } + + public void moveToNextQuestion() { + currentIndex++; + } + + public void incrementScore() { + score++; + } + + public Set getTopics() { + return topics; + } + + public int getTotalQuestions() { + return questions.size(); + } + + public int getCurrentIndex() { + return currentIndex; + } + + public int getScore() { + return score; + } +} diff --git a/src/main/java/com/javarush/zyibin/model/Question.java b/src/main/java/com/javarush/zyibin/model/Question.java new file mode 100644 index 0000000..d3b8cdf --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/Question.java @@ -0,0 +1,34 @@ +package com.javarush.zyibin.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class Question { + + private final String questionText; + private final List answers; + private final int correctAnswerIndex; + + @JsonCreator + public Question(@JsonProperty("questionText") String questionText, + @JsonProperty("answers") List answers, + @JsonProperty("correctAnswerIndex") int correctAnswerIndex) { + this.questionText = questionText; + this.answers = answers; + this.correctAnswerIndex = correctAnswerIndex; + } + + public String getQuestionText() { + return questionText; + } + + public List getAnswers() { + return answers; + } + + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } +} diff --git a/src/main/java/com/javarush/zyibin/model/Role.java b/src/main/java/com/javarush/zyibin/model/Role.java new file mode 100644 index 0000000..b280d37 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/Role.java @@ -0,0 +1,7 @@ +package com.javarush.zyibin.model; + +public enum Role { + + USER, + ADMIN +} diff --git a/src/main/java/com/javarush/zyibin/model/TestResult.java b/src/main/java/com/javarush/zyibin/model/TestResult.java new file mode 100644 index 0000000..c44923e --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/TestResult.java @@ -0,0 +1,74 @@ +package com.javarush.zyibin.model; + +import com.javarush.zyibin.util.TopicUtils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class TestResult { + + private long id; + private long userId; + private String topicCode; + private int totalQuestions; + private int correctAnswers; + private boolean passed; + private LocalDateTime finishedAt; + + public TestResult(long userId, + String topicCode, + int totalQuestions, + int correctAnswers, + boolean passed, + LocalDateTime finishedAt) { + this.userId = userId; + this.topicCode = topicCode; + this.totalQuestions = totalQuestions; + this.correctAnswers = correctAnswers; + this.passed = passed; + this.finishedAt = finishedAt; + } + + public void setId(long id) { + if (this.id != 0) { + throw new IllegalStateException("id is already set"); + } + this.id = id; + } + + public long getId() { + return id; + } + + public long getUserId() { + return userId; + } + + public String getTopicCode() { + return topicCode; + } + + public int getTotalQuestions() { + return totalQuestions; + } + + public int getCorrectAnswers() { + return correctAnswers; + } + + public boolean isPassed() { + return passed; + } + + public LocalDateTime getFinishedAt() { + return finishedAt; + } + + public String getTopicDisplayName() { + return TopicUtils.convertTopicCodesToDisplayNames(topicCode); + } + + public String getFormattedFinishedAt() { + return finishedAt.format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")); + } +} diff --git a/src/main/java/com/javarush/zyibin/model/Topic.java b/src/main/java/com/javarush/zyibin/model/Topic.java new file mode 100644 index 0000000..0812747 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/Topic.java @@ -0,0 +1,38 @@ +package com.javarush.zyibin.model; + +public enum Topic { + + JAVA_SYNTAX("java-syntax", "Java Syntax"), + JAVA_CORE("java-core", "Java Core"), + JAVA_CONCURRENCY("java-concurrency", "Java Concurrency"), + SERVLETS("servlets", "Сервлеты"), + MAVEN("maven", "Maven"), + JUNIT("junit5", "JUnit 5"), + MOCKITO("mockito", "Mockito"), + LOGGING("logging", "Logging"); + + private final String code; + private final String displayName; + + Topic(String code, String displayName) { + this.code = code; + this.displayName = displayName; + } + + public String getCode() { + return code; + } + + public String getDisplayName() { + return displayName; + } + + public static Topic fromCode(String code) { + for (Topic topic : values()) { + if (topic.code.equals(code)) { + return topic; + } + } + throw new IllegalArgumentException("Unknown topic code: " + code); + } +} diff --git a/src/main/java/com/javarush/zyibin/model/User.java b/src/main/java/com/javarush/zyibin/model/User.java new file mode 100644 index 0000000..c85ff91 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/model/User.java @@ -0,0 +1,100 @@ +package com.javarush.zyibin.model; + +import java.time.LocalDateTime; + + +public class User { + + private long id; + private final String username; + private final String passwordHash; + private final String email; + private String nickname; + private String about; + private String avatarPath; + private Role role; + private final LocalDateTime createdAt; + private boolean blocked; + + public User(long id, String username, String passwordHash, String email, Role role) { + this.id = id; + this.username = username; + this.passwordHash = passwordHash; + this.email = email; + this.role = role; + + this.nickname = username; + this.about = ""; + this.avatarPath = "/avatars/default/default.png"; + this.createdAt = LocalDateTime.now(); + + } + + public long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getPasswordHash() { + return passwordHash; + } + + public String getEmail() { + return email; + } + + public String getNickname() { + return nickname; + } + + public String getAbout() { + return about; + } + + public String getAvatarPath() { + return avatarPath; + } + + public Role getRole() { + return role; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public void setAbout(String about) { + this.about = about; + } + + public void setAvatarPath(String avatarPath) { + this.avatarPath = avatarPath; + } + + public boolean isBlocked() { + return blocked; + } + + public void setBlocked(boolean blocked) { + this.blocked = blocked; + } + + public void changeRole(Role role) { + this.role = role; + } + + public void setId(long id) { + if (this.id != 0) { + throw new IllegalStateException("ID пользователя уже установлен"); + } + this.id = id; + } +} + diff --git a/src/main/java/com/javarush/zyibin/repository/InMemoryTestResultRepository.java b/src/main/java/com/javarush/zyibin/repository/InMemoryTestResultRepository.java new file mode 100644 index 0000000..6be2241 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/repository/InMemoryTestResultRepository.java @@ -0,0 +1,51 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.TestResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class InMemoryTestResultRepository implements TestResultRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryTestResultRepository.class); + + private ConcurrentHashMap results = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + + @Override + public void save(TestResult result) { + if (result.getId() == 0) { + result.setId(idGenerator.getAndIncrement()); + } + results.put(result.getId(), result); + log.info("TestResult saved: id={}, userId={}, topics={}, passed={}", + result.getId(), + result.getUserId(), + result.getTopicCode(), + result.isPassed() + ); + } + + @Override + public List findByUserId(long userId) { + List list = new ArrayList<>(); + for (TestResult result : results.values()) { + if (result.getUserId() == userId) { + list.add(result); + } + } + log.debug("Loaded {} test results for userId={}", list.size(), userId); + return list; + } + + @Override + public List findAll() { + List all = new ArrayList<>(results.values()); + log.debug("Loaded all test results, count={}", all.size()); + return new ArrayList<>(results.values()); + } +} diff --git a/src/main/java/com/javarush/zyibin/repository/InMemoryUserRepository.java b/src/main/java/com/javarush/zyibin/repository/InMemoryUserRepository.java new file mode 100644 index 0000000..93cc21e --- /dev/null +++ b/src/main/java/com/javarush/zyibin/repository/InMemoryUserRepository.java @@ -0,0 +1,56 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class InMemoryUserRepository implements UserRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + + private final Map usersById = new ConcurrentHashMap<>(); + + private final Map usersByUserName = new ConcurrentHashMap<>(); + + private final AtomicLong idGenerator = new AtomicLong(1); + + @Override + public void save(User user) { + if (user.getId() == 0) { + user.setId(idGenerator.getAndIncrement()); + } + usersById.put(user.getId(), user); + usersByUserName.put(user.getUsername(), user); + log.info("User saved: id={}, username={}, new={}", + user.getId(), + user.getUsername(), + user.getId() == idGenerator.get() - 1); + } + + @Override + public Optional findByUserName(String username) { + Optional user = Optional.ofNullable(usersByUserName.get(username)); + log.debug("findByUserName called: username={}, found={}", username, user.isPresent()); + return user; + } + + @Override + public Optional findById(long id) { + Optional user = Optional.ofNullable(usersById.get(id)); + log.debug("findById called: id={}, found={}", id, user.isPresent()); + return user; + } + + @Override + public List findAll() { + ArrayList users = new ArrayList<>(usersById.values()); + log.debug("findAll called: count={}", users.size()); + return users; + } +} diff --git a/src/main/java/com/javarush/zyibin/repository/QuestionRepository.java b/src/main/java/com/javarush/zyibin/repository/QuestionRepository.java new file mode 100644 index 0000000..16609c5 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/repository/QuestionRepository.java @@ -0,0 +1,31 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.source.FileQuestionSource; +import com.javarush.zyibin.source.QuestionSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class QuestionRepository { + private static final Logger log = LoggerFactory.getLogger(QuestionRepository.class); + + private final QuestionSource source; + + public QuestionRepository(QuestionSource source) { + this.source = source; + } + + public static QuestionRepository defaultRepository() { + return new QuestionRepository(new FileQuestionSource()); + } + + public List getQuestions(Topic topic) { + log.debug("Loading questions for topic={}", topic); + List questions = source.loadQuestions(topic); + log.info("Questions loaded for topic={}, count={}", topic, questions.size()); + return questions; + } +} diff --git a/src/main/java/com/javarush/zyibin/repository/TestResultRepository.java b/src/main/java/com/javarush/zyibin/repository/TestResultRepository.java new file mode 100644 index 0000000..6c45990 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/repository/TestResultRepository.java @@ -0,0 +1,14 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.TestResult; + +import java.util.List; + +public interface TestResultRepository { + + void save(TestResult result); + + List findByUserId(long userId); + + List findAll(); +} diff --git a/src/main/java/com/javarush/zyibin/repository/UserRepository.java b/src/main/java/com/javarush/zyibin/repository/UserRepository.java new file mode 100644 index 0000000..64f9c78 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/repository/UserRepository.java @@ -0,0 +1,17 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.User; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository { + + void save(User user); + + Optional findByUserName(String username); + + Optional findById(long id); + + List findAll(); +} diff --git a/src/main/java/com/javarush/zyibin/service/AdminUserService.java b/src/main/java/com/javarush/zyibin/service/AdminUserService.java new file mode 100644 index 0000000..86a1046 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/AdminUserService.java @@ -0,0 +1,41 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AdminUserService { + private static final Logger log = LoggerFactory.getLogger(AdminUserService.class); + + private final UserRepository userRepository; + + public AdminUserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public void changeUserRole(long adminId, long targetUserId, Role newRole) { + log.info("Admin {} attempts to change role of user {} to {}", + adminId, + targetUserId, + newRole + ); + + userRepository.findById(targetUserId).ifPresentOrElse(user -> { + if (user.getId() == adminId) { + log.warn("Admin {} attempted to change own role. Operation denied", adminId); + return; + } + user.changeRole(newRole); + log.info("User {} role changed to {} by admin {}", + targetUserId, + newRole, + adminId + ); + }, () -> { + log.warn("Admin {} attempted to change role of non-existing user {}", + adminId, + targetUserId); + }); + } +} diff --git a/src/main/java/com/javarush/zyibin/service/AuthenticationService.java b/src/main/java/com/javarush/zyibin/service/AuthenticationService.java new file mode 100644 index 0000000..f97b01b --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/AuthenticationService.java @@ -0,0 +1,43 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.exception.AuthenticationException; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import com.javarush.zyibin.util.PasswordUtil; +import com.javarush.zyibin.util.ValidationFactory; +import com.javarush.zyibin.validation.UserValidation; + +import java.util.Optional; + +public class AuthenticationService { + + private final UserRepository userRepository; + private final UserValidation userValidator; + + public AuthenticationService(UserRepository userRepository) { + this.userRepository = userRepository; + this.userValidator = ValidationFactory.createUserValidator(); + } + + public User authenticate(String username, String password) { + userValidator.validateLogin(username, password); + + Optional userOptional = userRepository.findByUserName(username); + if (userOptional.isEmpty()) { + throw AuthenticationException.userNotFound(); + } + + User user = userOptional.get(); + + String passwordHash = PasswordUtil.hashPassword(password); + if (!user.getPasswordHash().equals(passwordHash)) { + throw AuthenticationException.invalidCredentials(); + } + + if (user.isBlocked()) { + throw AuthenticationException.userBlocked(); + } + + return user; + } +} diff --git a/src/main/java/com/javarush/zyibin/service/AvatarService.java b/src/main/java/com/javarush/zyibin/service/AvatarService.java new file mode 100644 index 0000000..5fbb374 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/AvatarService.java @@ -0,0 +1,22 @@ +package com.javarush.zyibin.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class AvatarService { + private static final Logger log = LoggerFactory.getLogger(AvatarService.class); + + public List getAvailableAvatars() { + log.debug("Loading available avatars list"); + return List.of( + "/resources/avatars/default/avatar1.png", + "/resources/avatars/default/avatar2.png", + "/resources/avatars/default/avatar3.png", + "/resources/avatars/default/avatar4.png", + "/resources/avatars/default/avatar5.png", + "/resources/avatars/default/avatar6.png" + ); + } +} diff --git a/src/main/java/com/javarush/zyibin/service/QuestionService.java b/src/main/java/com/javarush/zyibin/service/QuestionService.java new file mode 100644 index 0000000..d99759a --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/QuestionService.java @@ -0,0 +1,34 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.InterviewState; +import com.javarush.zyibin.util.ValidationFactory; +import com.javarush.zyibin.validation.QuestionValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class QuestionService { + + private static final Logger log = LoggerFactory.getLogger(QuestionService.class); + private final QuestionValidator questionValidator; + + public QuestionService() { + this.questionValidator = ValidationFactory.createQuestionValidator(); + } + + public void processAnswer(InterviewState state, String answerIndexStr) { + questionValidator.validateAnswer(answerIndexStr, state.getCurrentQuestion().getAnswers().size() - 1); + + int selectedIndex = Integer.parseInt(answerIndexStr); + + if (selectedIndex == state.getCurrentQuestion().getCorrectAnswerIndex()) { + log.debug("Correct answer selected"); + state.incrementScore(); + } else { + log.debug("Incorrect answer selected"); + } + + state.moveToNextQuestion(); + log.debug("Moving to next question, current index is now {}", state.getCurrentIndex()); + } +} diff --git a/src/main/java/com/javarush/zyibin/service/RegistrationService.java b/src/main/java/com/javarush/zyibin/service/RegistrationService.java new file mode 100644 index 0000000..af6d4d1 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/RegistrationService.java @@ -0,0 +1,23 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.util.ValidationFactory; +import com.javarush.zyibin.validation.UserValidation; + + +public class RegistrationService { + + private final UserService userService; + private final UserValidation userValidator; + + public RegistrationService(UserService userService) { + this.userService = userService; + this.userValidator = ValidationFactory.createUserValidator(); + } + + public User registerUser(String username, String password, String email) { + userValidator.validateRegistration(username, password, email); + + return userService.register(username, password, email); + } +} diff --git a/src/main/java/com/javarush/zyibin/service/UserService.java b/src/main/java/com/javarush/zyibin/service/UserService.java new file mode 100644 index 0000000..736b6d4 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/UserService.java @@ -0,0 +1,8 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.User; + +public interface UserService { + + User register(String username, String rawPassword, String email); +} diff --git a/src/main/java/com/javarush/zyibin/service/UserServiceImpl.java b/src/main/java/com/javarush/zyibin/service/UserServiceImpl.java new file mode 100644 index 0000000..f9fa9ad --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/UserServiceImpl.java @@ -0,0 +1,49 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import com.javarush.zyibin.util.PasswordUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UserServiceImpl implements UserService { + private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); + + private final UserRepository userRepository; + + + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public User register(String username, String rawPassword, String email) { + log.info("User registration attempt: username={}", username); + Role role = username.equals("admin") + ? Role.ADMIN + : Role.USER; + userRepository.findByUserName(username) + .ifPresent(u -> { + log.warn("Registration failed: username {} already exists", username); + throw new IllegalStateException("User with this login already exists"); + }); + String passwordHash = PasswordUtil.hashPassword(rawPassword); + User user = new User( + 0, + username, + passwordHash, + email, + role + ); + + userRepository.save(user); + log.info("User registered successfully: id={}, username={}, role={}", + user.getId(), + user.getUsername(), + user.getRole() + ); + return user; + } + +} diff --git a/src/main/java/com/javarush/zyibin/service/UserStatisticsService.java b/src/main/java/com/javarush/zyibin/service/UserStatisticsService.java new file mode 100644 index 0000000..6d9271e --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/UserStatisticsService.java @@ -0,0 +1,11 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTopicStats; +import com.javarush.zyibin.model.TestResult; + +import java.util.List; + +public interface UserStatisticsService { + + List calculateUserTopicStats(List results); +} diff --git a/src/main/java/com/javarush/zyibin/service/UserStatisticsServiceImpl.java b/src/main/java/com/javarush/zyibin/service/UserStatisticsServiceImpl.java new file mode 100644 index 0000000..0ddf82d --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/UserStatisticsServiceImpl.java @@ -0,0 +1,40 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTopicStats; +import com.javarush.zyibin.model.TestResult; +import com.javarush.zyibin.model.Topic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UserStatisticsServiceImpl implements UserStatisticsService { + private static final Logger log = LoggerFactory.getLogger(UserStatisticsServiceImpl.class); + + @Override + public List calculateUserTopicStats(List results) { + log.debug("Calculating user topic statistics, results count={}", results.size()); + Map statsByTopic = new HashMap<>(); + for (TestResult result : results) { + String[] topicCodes = result.getTopicCode().split(","); + for (String topicCode : topicCodes) { + String code = topicCode.trim(); + Topic topic = Topic.fromCode(code); + String displayName = topic.getDisplayName(); + UserTopicStats stats = statsByTopic.computeIfAbsent( + displayName, + UserTopicStats::new + ); + stats.incrementTotal(); + if (result.isPassed()) { + stats.incrementPassed(); + } + } + } + log.debug("User topic statistics calculated, topics count={}", statsByTopic.size()); + return new ArrayList<>(statsByTopic.values()); + } +} diff --git a/src/main/java/com/javarush/zyibin/service/UserTestStatisticsService.java b/src/main/java/com/javarush/zyibin/service/UserTestStatisticsService.java new file mode 100644 index 0000000..671054f --- /dev/null +++ b/src/main/java/com/javarush/zyibin/service/UserTestStatisticsService.java @@ -0,0 +1,38 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTestStats; +import com.javarush.zyibin.model.TestResult; +import com.javarush.zyibin.util.TopicUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UserTestStatisticsService { + private static final Logger log = LoggerFactory.getLogger(UserTestStatisticsService.class); + + public List calculate(List results) { + log.debug("Calculating user test statistics, results count={}", results.size()); + Map map = new HashMap<>(); + + for (TestResult result : results) { + String testKey = buildTestName(result.getTopicCode()); + UserTestStats stats = + map.computeIfAbsent(testKey, UserTestStats::new); + stats.incrementTotal(); + if (result.isPassed()) { + stats.incrementPassed(); + } + } + log.debug("User test statistics calculated, tests count={}", map.size()); + return new ArrayList<>(map.values()); + } + + private String buildTestName(String topicCode) { + return TopicUtils.convertTopicCodesToDisplayNames(topicCode); + } +} + diff --git a/src/main/java/com/javarush/zyibin/session/SessionKeys.java b/src/main/java/com/javarush/zyibin/session/SessionKeys.java new file mode 100644 index 0000000..25b1c4c --- /dev/null +++ b/src/main/java/com/javarush/zyibin/session/SessionKeys.java @@ -0,0 +1,10 @@ +package com.javarush.zyibin.session; + +public class SessionKeys { + public static final String QUESTIONS = "questions"; + public static final String CURRENT_INDEX = "currentIndex"; + public static final String SCORE = "score"; + + private SessionKeys() { + } +} diff --git a/src/main/java/com/javarush/zyibin/session/SessionUtils.java b/src/main/java/com/javarush/zyibin/session/SessionUtils.java new file mode 100644 index 0000000..049d02e --- /dev/null +++ b/src/main/java/com/javarush/zyibin/session/SessionUtils.java @@ -0,0 +1,25 @@ +package com.javarush.zyibin.session; + +import com.javarush.zyibin.model.InterviewState; +import jakarta.servlet.http.HttpSession; + +public class SessionUtils { + + public static final String INTERVIEW_STATE = "interviewState"; + + public static boolean hasInterview(HttpSession session) { + return session != null && session.getAttribute(INTERVIEW_STATE) != null; + } + + public static InterviewState getInterviewState(HttpSession session) { + return (InterviewState) session.getAttribute(INTERVIEW_STATE); + } + + public static void setInterviewState(HttpSession session, InterviewState state) { + session.setAttribute(INTERVIEW_STATE, state); + } + + public static void clearInterview(HttpSession session) { + session.removeAttribute(INTERVIEW_STATE); + } +} diff --git a/src/main/java/com/javarush/zyibin/source/FileQuestionSource.java b/src/main/java/com/javarush/zyibin/source/FileQuestionSource.java new file mode 100644 index 0000000..7b3a0d8 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/source/FileQuestionSource.java @@ -0,0 +1,52 @@ +package com.javarush.zyibin.source; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.util.ValidationFactory; +import com.javarush.zyibin.validation.QuestionValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.List; + +public class FileQuestionSource implements QuestionSource { + private static final Logger log = LoggerFactory.getLogger(FileQuestionSource.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String QUESTIONS_FOLDER = "questions/"; + + + @Override + public List loadQuestions(Topic topic) { + String fileName = QUESTIONS_FOLDER + topic.getCode() + ".json"; + + log.debug("Loading questions from file={}", fileName); + + ClassLoader classLoader = getClass().getClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream(fileName); + + if (inputStream == null) { + log.error("Questions file not found: {}", fileName); + throw new IllegalStateException("File not found in resources: " + fileName); + } + + try { + List questions = objectMapper.readValue( + inputStream, + new TypeReference>() { + } + ); + QuestionValidator questionValidator = ValidationFactory.createQuestionValidator(); + questionValidator.validate(questions); + log.info("Questions loaded successfully: topic={}, count={}", topic, questions.size()); + return questions; + } catch (Exception e) { + log.error("Error reading questions file: {}", fileName, e); + throw new IllegalStateException("Error reading: " + fileName, e); + } + } +} diff --git a/src/main/java/com/javarush/zyibin/source/QuestionSource.java b/src/main/java/com/javarush/zyibin/source/QuestionSource.java new file mode 100644 index 0000000..98cb1f8 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/source/QuestionSource.java @@ -0,0 +1,11 @@ +package com.javarush.zyibin.source; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; + +import java.util.List; + +public interface QuestionSource { + + List loadQuestions(Topic topic); +} diff --git a/src/main/java/com/javarush/zyibin/util/PasswordUtil.java b/src/main/java/com/javarush/zyibin/util/PasswordUtil.java new file mode 100644 index 0000000..4a1a3f9 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/util/PasswordUtil.java @@ -0,0 +1,23 @@ +package com.javarush.zyibin.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.HexFormat; + +public class PasswordUtil { + private static final String ALGORITHM = "SHA-256"; + + private PasswordUtil() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + public static String hashPassword(String password) { + try { + MessageDigest digest = MessageDigest.getInstance(ALGORITHM); + byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to hash password", e); + } + } +} diff --git a/src/main/java/com/javarush/zyibin/util/TopicUtils.java b/src/main/java/com/javarush/zyibin/util/TopicUtils.java new file mode 100644 index 0000000..d8bbb8d --- /dev/null +++ b/src/main/java/com/javarush/zyibin/util/TopicUtils.java @@ -0,0 +1,25 @@ +package com.javarush.zyibin.util; + +import com.javarush.zyibin.model.Topic; + +import java.util.ArrayList; +import java.util.List; + +public class TopicUtils { + + public static String convertTopicCodesToDisplayNames(String topicCodes) { + if (topicCodes == null || topicCodes.trim().isEmpty()) { + return ""; + } + + String[] codes = topicCodes.split(","); + List names = new ArrayList<>(); + + for (String code : codes) { + Topic topic = Topic.fromCode(code.trim()); + names.add(topic.getDisplayName()); + } + + return String.join(", ", names); + } +} diff --git a/src/main/java/com/javarush/zyibin/util/ValidationFactory.java b/src/main/java/com/javarush/zyibin/util/ValidationFactory.java new file mode 100644 index 0000000..4f2e223 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/util/ValidationFactory.java @@ -0,0 +1,23 @@ +package com.javarush.zyibin.util; + +import com.javarush.zyibin.handler.ErrorHandler; +import com.javarush.zyibin.validation.QuestionValidator; +import com.javarush.zyibin.validation.UserValidation; + +public class ValidationFactory { + + private ValidationFactory() { + } + + public static UserValidation createUserValidator() { + return new UserValidation(); + } + + public static QuestionValidator createQuestionValidator() { + return new QuestionValidator(); + } + + public static ErrorHandler createErrorHandler() { + return new ErrorHandler(); + } +} diff --git a/src/main/java/com/javarush/zyibin/validation/QuestionValidator.java b/src/main/java/com/javarush/zyibin/validation/QuestionValidator.java new file mode 100644 index 0000000..b7b5ea4 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/validation/QuestionValidator.java @@ -0,0 +1,62 @@ +package com.javarush.zyibin.validation; + +import com.javarush.zyibin.exception.ValidationException; +import com.javarush.zyibin.model.Question; + +import java.util.List; + +public class QuestionValidator { + + public static void validate(List questions) { + if (questions == null || questions.isEmpty()) { + throw ValidationException.general("The list of questions is empty"); + } + for (int i = 0; i < questions.size(); i++) { + validateQuestion(questions.get(i), i); + } + } + + private static void validateQuestion(Question question, int index) { + if (question == null) { + throw ValidationException.general("Index issue " + index + " equals null"); + } + if (question.getQuestionText() == null || + question.getQuestionText().isBlank()) { + throw ValidationException.general("Blank question text (index=" + index + ")"); + } + int correctIndex = question.getCorrectAnswerIndex(); + if (question.getAnswers() == null || question.getAnswers().isEmpty()) { + throw ValidationException.general("Answers list is empty (index=" + index + ")"); + } + if (correctIndex < 0 || correctIndex >= question.getAnswers().size()) { + throw ValidationException.general("Incorrect correctAnswerIndex (index=" + index + ")"); + } + } + + public void validateTopics(String[] topics) { + if (topics == null || topics.length == 0) { + throw ValidationException.general("Please select at least one topic"); + } + + if (topics.length > 5) { + throw ValidationException.general("Please select no more than 5 topics"); + } + } + + public void validateAnswer(String answerIndexStr, int maxIndex) { + if (answerIndexStr == null || answerIndexStr.trim().isEmpty()) { + throw ValidationException.general("Please select an answer"); + } + + try { + int answerIndex = Integer.parseInt(answerIndexStr); + + if (answerIndex < 0 || answerIndex > maxIndex) { + throw ValidationException.general("Invalid answer selected"); + } + + } catch (NumberFormatException e) { + throw ValidationException.general("Invalid answer format"); + } + } +} diff --git a/src/main/java/com/javarush/zyibin/validation/UserValidation.java b/src/main/java/com/javarush/zyibin/validation/UserValidation.java new file mode 100644 index 0000000..67ed4a9 --- /dev/null +++ b/src/main/java/com/javarush/zyibin/validation/UserValidation.java @@ -0,0 +1,92 @@ +package com.javarush.zyibin.validation; + +import com.javarush.zyibin.exception.ValidationException; + +import java.util.regex.Pattern; + +public class UserValidation { + + private static final int MIN_USERNAME_LENGTH = 3; + private static final int MAX_USERNAME_LENGTH = 20; + private static final int MIN_PASSWORD_LENGTH = 6; + private static final int MAX_PASSWORD_LENGTH = 100; + private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$"; + private static final String USERNAME_REGEX = "^[a-zA-Z0-9_-]+$"; + + private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + private static final Pattern USERNAME_PATTERN = Pattern.compile(USERNAME_REGEX); + + public void validateRegistration(String username, String password, String email) { + validateUsername(username); + validatePassword(password); + validateEmail(email); + } + + public void validateLogin(String username, String password) { + if (username == null || username.trim().isEmpty()) { + throw ValidationException.username("Username cannot be empty"); + } + if (password == null || password.isEmpty()) { + throw ValidationException.password("Password cannot be empty"); + } + } + + public void validateUsername(String username) { + if (username == null || username.trim().isEmpty()) { + throw ValidationException.username("Username cannot be empty"); + } + String trimmedUsername = username.trim(); + if (trimmedUsername.length() < MIN_USERNAME_LENGTH) { + throw ValidationException.username(String.format("Username must be at least %d characters", MIN_USERNAME_LENGTH)); + } + if (trimmedUsername.length() > MAX_USERNAME_LENGTH) { + throw ValidationException.username(String.format("Username must be at most %d characters", MAX_USERNAME_LENGTH)); + } + if (!USERNAME_PATTERN.matcher(trimmedUsername).matches()) { + throw ValidationException.username("Username can only contain letters, numbers, underscores and hyphens"); + } + } + + public void validatePassword(String password) { + if (password == null || password.isEmpty()) { + throw ValidationException.password("Password cannot be empty"); + } + + if (password.length() < MIN_PASSWORD_LENGTH) { + throw ValidationException.password( + String.format("Password must be at least %d characters", MIN_PASSWORD_LENGTH) + ); + } + + if (password.length() > MAX_PASSWORD_LENGTH) { + throw ValidationException.password( + String.format("Password must be no more than %d characters", MAX_PASSWORD_LENGTH) + ); + } + } + + public void validateEmail(String email) { + if (email == null || email.trim().isEmpty()) { + throw ValidationException.email("Email cannot be empty"); + } + + String trimmedEmail = email.trim(); + + if (!EMAIL_PATTERN.matcher(trimmedEmail).matches()) { + throw ValidationException.email("Invalid email format"); + } + + if (trimmedEmail.length() > 100) { + throw ValidationException.email("Email is too long"); + } + } + + public void validateProfile(String nickname, String about) { + if (nickname != null && nickname.length() > 50) { + throw ValidationException.general("Nickname is too long (max 50 characters)"); + } + if (about != null && about.length() > 500) { + throw ValidationException.general("About section is too long (max 500 characters)"); + } + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..a88a6e4 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,126 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %c{1.} - %msg%n + + + ${sys:catalina.home}/logs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/questions/java-concurrency.json b/src/main/resources/questions/java-concurrency.json new file mode 100644 index 0000000..5e6b363 --- /dev/null +++ b/src/main/resources/questions/java-concurrency.json @@ -0,0 +1,839 @@ +[ + { + "questionText": "Какой класс используется для создания нового потока выполнения в Java?", + "answers": [ + "java.lang.Thread", + "java.util.concurrent.Process", + "java.lang.Runnable" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс необходимо реализовать для передачи задачи в поток?", + "answers": [ + "Callable", + "Runnable", + "Threadable" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод запускает выполнение потока?", + "answers": [ + "run()", + "start()", + "execute()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что произойдёт, если вызвать run() напрямую вместо start()?", + "answers": [ + "Создастся новый поток", + "Метод выполнится в текущем потоке", + "Будет выброшено исключение" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для приостановки выполнения текущего потока на заданное время?", + "answers": [ + "Thread.wait(1000)", + "Thread.sleep(1000)", + "Thread.pause(1000)" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов принадлежит объекту монитора (monitor)?", + "answers": [ + "Thread.sleep()", + "Object.wait()", + "Thread.yield()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой ключевой механизм используется для предотвращения одновременного доступа нескольких потоков к критической секции?", + "answers": [ + "synchronized", + "volatile", + "transient" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что гарантирует ключевое слово volatile?", + "answers": [ + "Атомарность операций", + "Видимость изменений между потоками", + "Полную потокобезопасность переменной" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих классов НЕ является потокобезопасным?", + "answers": [ + "ConcurrentHashMap", + "StringBuilder", + "AtomicInteger" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой интерфейс возвращает результат выполнения задачи и может выбрасывать проверяемое исключение?", + "answers": [ + "Runnable", + "Callable", + "FutureTask" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой объект используется для получения результата асинхронной задачи, отправленной через ExecutorService?", + "answers": [ + "Promise", + "Future", + "Callback" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как правильно завершить работу ExecutorService?", + "answers": [ + "Вызвать shutdown() или shutdownNow()", + "Установить ссылку в null", + "Вызвать close()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов НЕ блокирует текущий поток?", + "answers": [ + "Future.get()", + "Thread.sleep()", + "Thread.yield()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое состояние 'WAITING' у потока?", + "answers": [ + "Поток ждёт освобождения монитора", + "Поток ожидает вызова notify() или interrupt()", + "Поток приостановлен на фиксированное время" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для пробуждения одного ожидающего потока?", + "answers": [ + "notify()", + "notifyAll()", + "wakeup()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для пробуждения всех ожидающих потоков?", + "answers": [ + "notify()", + "notifyAll()", + "wakeAll()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Где можно использовать ключевое слово synchronized?", + "answers": [ + "Только в методах", + "В методах и блоках кода", + "Только в статических блоках" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что происходит при попытке двух потоков одновременно войти в synchronized-метод одного объекта?", + "answers": [ + "Оба выполняются параллельно", + "Один ждёт, пока другой завершит выполнение", + "Происходит deadlock" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих объектов используется как монитор в synchronized-блоке?", + "answers": [ + "this", + "Любой объект, указанный в скобках", + "Только статические поля" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой класс из java.util.concurrent предоставляет более гибкую альтернативу synchronized?", + "answers": [ + "ReentrantLock", + "ReadWriteLock", + "StampedLock" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов обеспечивает атомарные операции над целыми числами?", + "answers": [ + "Integer", + "AtomicInteger", + "VolatileInt" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое deadlock?", + "answers": [ + "Два потока бесконечно ждут друг друга, удерживая ресурсы", + "Поток завис из-за исключения", + "Поток завершился аварийно" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих подходов помогает избежать deadlock?", + "answers": [ + "Использовать один общий монитор", + "Всегда захватывать блокировки в одном и том же порядке", + "Избегать synchronized" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов вызывается внутри synchronized-блока или метода?", + "answers": [ + "Thread.sleep()", + "Object.wait()", + "Thread.yield()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что делает Thread.join()?", + "answers": [ + "Объединяет два потока в один", + "Текущий поток ждёт завершения указанного потока", + "Прерывает указанный поток" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих интерфейсов представляет пул потоков?", + "answers": [ + "ThreadPool", + "ExecutorService", + "ThreadFactory" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как создать фиксированный пул потоков с 4 потоками?", + "answers": [ + "Executors.newFixedThreadPool(4)", + "new ThreadPool(4)", + "ExecutorService.create(4)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов позволяет выполнять задачи по расписанию?", + "answers": [ + "ScheduledExecutorService", + "TimerTask", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для проверки, был ли прерван поток?", + "answers": [ + "Thread.isInterrupted()", + "Thread.interrupted()", + "Оба метода корректны, но отличаются поведением" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что делает Thread.interrupted()?", + "answers": [ + "Возвращает флаг прерывания и сбрасывает его", + "Только возвращает флаг без сброса", + "Прерывает текущий поток" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов НЕ поддерживает параллельные операции?", + "answers": [ + "ConcurrentHashMap", + "CopyOnWriteArrayList", + "ArrayList" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов ConcurrentHashMap является атомарным?", + "answers": [ + "putIfAbsent()", + "get()", + "Оба метода потокобезопасны, но только putIfAbsent — атомарная операция" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое happens-before в модели памяти Java?", + "answers": [ + "Гарантия, что изменения одного потока видны другому при определённых условиях", + "Порядок выполнения инструкций в одном потоке", + "Механизм компиляторной оптимизации" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих примитивов синхронизации позволяет нескольким читателям и одному писателю?", + "answers": [ + "ReentrantLock", + "ReadWriteLock", + "Semaphore" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой класс используется для ограничения количества потоков, обращающихся к ресурсу?", + "answers": [ + "CountDownLatch", + "CyclicBarrier", + "Semaphore" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой примитив синхронизации позволяет дождаться завершения фиксированного числа задач?", + "answers": [ + "CountDownLatch", + "Phaser", + "Exchanger" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой примитив позволяет повторно использовать барьер для синхронизации потоков?", + "answers": [ + "CountDownLatch", + "CyclicBarrier", + "Semaphore" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов вызывает InterruptedException?", + "answers": [ + "Thread.sleep()", + "Object.wait()", + "Оба метода" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих подходов рекомендуется для обработки InterruptedException?", + "answers": [ + "Игнорировать его", + "Восстановить флаг прерывания и/или пробросить исключение", + "Завершить JVM" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих классов используется для обмена данными между двумя потоками?", + "answers": [ + "Exchanger", + "SynchronousQueue", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов НЕ является частью ForkJoinPool?", + "answers": [ + "invoke()", + "submit()", + "scheduleAtFixedRate()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой интерфейс используется для рекурсивного разбиения задач в Fork/Join?", + "answers": [ + "RecursiveTask", + "ForkJoinTask", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов используется для асинхронного выполнения задачи без возврата результата?", + "answers": [ + "ExecutorService.execute(Runnable)", + "ExecutorService.submit(Runnable)", + "Оба метода, но execute не возвращает Future" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих классов позволяет выполнять цепочки асинхронных операций?", + "answers": [ + "Future", + "CompletableFuture", + "Promise" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как создать завершённый CompletableFuture с результатом?", + "answers": [ + "CompletableFuture.completedFuture(result)", + "new CompletableFuture().complete(result)", + "CompletableFuture.success(result)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод CompletableFuture выполняет действие после завершения задачи?", + "answers": [ + "thenApply()", + "thenAccept()", + "thenRun()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод CompletableFuture используется для обработки исключения?", + "answers": [ + "handle()", + "exceptionally()", + "Оба метода" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов объединяет два CompletableFuture?", + "answers": [ + "thenCombine()", + "allOf()", + "Оба метода" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое livelock?", + "answers": [ + "Потоки активны, но не могут продвинуться вперёд из-за взаимной реакции", + "То же самое, что deadlock", + "Потоки спят бесконечно" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих типов памяти используется для хранения состояния потока?", + "answers": [ + "Stack", + "Heap", + "Metaspace" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Где хранятся разделяемые объекты в многопоточной программе?", + "answers": [ + "В стеке каждого потока", + "В куче (heap)", + "В регистровой памяти CPU" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих подходов НЕ обеспечивает потокобезопасность?", + "answers": [ + "Использование локальных переменных", + "Использование final-полей", + "Использование несинхронизированных изменяемых полей" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих классов является immutable и, следовательно, потокобезопасен?", + "answers": [ + "String", + "StringBuilder", + "ArrayList" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов ReentrantLock аналогичен synchronized?", + "answers": [ + "lock() / unlock()", + "tryLock()", + "lockInterruptibly()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов позволяет избежать блокировки, если она недоступна?", + "answers": [ + "lock()", + "tryLock()", + "lockInterruptibly()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов ReentrantLock может быть прерван?", + "answers": [ + "lock()", + "lockInterruptibly()", + "tryLock()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих интерфейсов расширяет Lock и поддерживает несколько читателей?", + "answers": [ + "ReadWriteLock", + "StampedLock", + "ReentrantReadWriteLock" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов предоставляет оптимистичную блокировку?", + "answers": [ + "ReentrantLock", + "StampedLock", + "Semaphore" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для атомарного увеличения значения AtomicInteger?", + "answers": [ + "increment()", + "add(1)", + "incrementAndGet()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов AtomicReference позволяет атомарно обновить значение на основе предыдущего?", + "answers": [ + "set()", + "updateAndGet()", + "compareAndSet()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что делает метод compareAndSet(expect, update) в Atomic-классах?", + "answers": [ + "Всегда устанавливает новое значение", + "Устанавливает значение, только если текущее равно ожидаемому", + "Сравнивает два значения и возвращает true, если равны" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих подходов минимизирует конкуренцию за блокировки?", + "answers": [ + "Использование одного глобального монитора", + "Разделение данных (lock splitting)", + "Увеличение времени удержания блокировки" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для ожидания условия в связке с Lock?", + "answers": [ + "Condition.await()", + "Object.wait()", + "Thread.sleep()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Сколько Condition-объектов может быть связано с одним ReentrantLock?", + "answers": [ + "Один", + "Несколько", + "Ни одного" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для сигнализации одному потоку, ожидающему на Condition?", + "answers": [ + "signal()", + "notify()", + "wake()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для сигнализации всем потокам, ожидающим на Condition?", + "answers": [ + "signalAll()", + "notifyAll()", + "wakeAll()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов используется для асинхронного выполнения задач с возможностью отмены?", + "answers": [ + "FutureTask", + "CompletableFuture", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как отменить задачу, представленную Future?", + "answers": [ + "future.cancel(true)", + "future.stop()", + "future.abort()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что означает параметр mayInterruptIfRunning в future.cancel()?", + "answers": [ + "Прерывать поток, если задача уже выполняется", + "Отменять задачу только если она ещё не началась", + "Игнорировать отмену, если задача завершена" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов CompletableFuture выполняет задачу в отдельном потоке?", + "answers": [ + "thenApply()", + "thenApplyAsync()", + "thenCompose()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для выполнения задачи после завершения любого из двух CompletableFuture?", + "answers": [ + "anyOf()", + "allOf()", + "combine()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для выполнения задачи после завершения всех CompletableFuture?", + "answers": [ + "anyOf()", + "allOf()", + "joinAll()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих подходов НЕ рекомендуется в многопоточном коде?", + "answers": [ + "Использование ThreadLocal для изоляции состояния", + "Вызов неблокирующих методов", + "Использование System.out.println() в критической секции" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих классов позволяет безопасно публиковать объект между потоками?", + "answers": [ + "volatile-ссылка", + "final-поле", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое safe publication в Java?", + "answers": [ + "Публикация объекта через синхронизацию, volatile, final или concurrent-коллекции", + "Публикация через REST API", + "Использование публичных методов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для ожидания завершения всех задач в пуле?", + "answers": [ + "executor.awaitTermination()", + "executor.wait()", + "executor.join()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов ExecutorService возвращает Future<Void>?", + "answers": [ + "submit(Runnable)", + "submit(Callable)", + "execute(Runnable)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов реализует интерфейс BlockingQueue?", + "answers": [ + "ArrayBlockingQueue", + "LinkedBlockingQueue", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов BlockingQueue блокирует поток, если очередь пуста?", + "answers": [ + "poll()", + "take()", + "peek()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов BlockingQueue НЕ блокирует поток?", + "answers": [ + "offer()", + "put()", + "take()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов используется для передачи одного элемента между потоками?", + "answers": [ + "SynchronousQueue", + "LinkedTransferQueue", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов используется для передачи элемента в TransferQueue?", + "answers": [ + "transfer()", + "put()", + "send()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих классов поддерживает приоритетную очередь в многопоточной среде?", + "answers": [ + "PriorityBlockingQueue", + "PriorityQueue", + "TreeBlockingQueue" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для проверки состояния Future?", + "answers": [ + "isDone()", + "isCompleted()", + "isFinished()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для проверки, была ли задача отменена?", + "answers": [ + "isCancelled()", + "isAborted()", + "isInterrupted()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих подходов обеспечивает наилучшую масштабируемость при высокой конкуренции?", + "answers": [ + "synchronized", + "ReentrantLock", + "Non-blocking algorithms (CAS)" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих классов использует CAS (Compare-And-Swap) внутри?", + "answers": [ + "AtomicInteger", + "synchronized-блок", + "ReentrantLock" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для явного указания порядка памяти в VarHandle (Java 9+)?", + "answers": [ + "MemoryOrder", + "MemoryMode", + "VarHandle.releaseStore()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих инструментов помогает обнаружить гонки данных в Java?", + "answers": [ + "jstack", + "ThreadSanitizer (через native)", + "jcstress" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих методов используется для получения ID потока?", + "answers": [ + "Thread.getId()", + "Thread.currentThread().getId()", + "Оба варианта верны" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для установки имени потока?", + "answers": [ + "Thread.setName()", + "Thread.name = ...", + "Thread.current().setName()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих методов используется для получения списка всех живых потоков?", + "answers": [ + "Thread.getAllThreads()", + "Thread.enumerate()", + "Thread.activeCount()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих методов используется для уступки процессорного времени другим потокам?", + "answers": [ + "Thread.sleep(0)", + "Thread.yield()", + "Thread.wait(0)" + ], + "correctAnswerIndex": 1 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/java-core.json b/src/main/resources/questions/java-core.json new file mode 100644 index 0000000..c572338 --- /dev/null +++ b/src/main/resources/questions/java-core.json @@ -0,0 +1,911 @@ +[ + { + "questionText": "Что такое JVM?", + "answers": [ + "Среда выполнения Java-приложений", + "Компилятор Java", + "Фреймворк для веб-приложений" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип данных в Java является примитивным?", + "answers": [ + "String", + "Integer", + "int" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что означает ключевое слово 'final' при объявлении переменной?", + "answers": [ + "Переменная может быть изменена только один раз", + "Переменная не может быть изменена после инициализации", + "Переменная автоматически удаляется сборщиком мусора" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой модификатор доступа позволяет доступ только внутри одного пакета?", + "answers": [ + "private", + "protected", + "package-private (по умолчанию)" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое интерфейс в Java?", + "answers": [ + "Класс, содержащий только статические методы", + "Абстрактный тип, определяющий контракт поведения", + "Механизм множественного наследования реализации" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается при запуске Java-приложения?", + "answers": [ + "start()", + "init()", + "main(String[] args)" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое исключение (Exception) в Java?", + "answers": [ + "Ошибка компиляции", + "Событие, нарушающее нормальный поток выполнения программы", + "Предупреждение от компилятора" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из перечисленных типов не является обёрткой для примитива?", + "answers": [ + "Double", + "String", + "Boolean" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что делает оператор 'new' в Java?", + "answers": [ + "Объявляет новую переменную", + "Вызывает сборщик мусора", + "Создаёт новый объект в куче" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой класс является родителем всех классов в Java?", + "answers": [ + "Object", + "Class", + "Root" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое полиморфизм?", + "answers": [ + "Возможность одного и того же метода работать по-разному в зависимости от объекта", + "Наследование от нескольких классов", + "Использование только статических методов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой модификатор доступа самый строгий?", + "answers": [ + "private", + "protected", + "public" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое абстрактный класс?", + "answers": [ + "Класс, который нельзя инстанцировать и может содержать абстрактные методы", + "Класс без конструктора", + "Класс, содержащий только final-методы" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих циклов не существует в Java?", + "answers": [ + "for", + "foreach", + "repeat-until" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое autoboxing?", + "answers": [ + "Автоматическое преобразование примитива в объект обёртки", + "Упаковка нескольких объектов в один массив", + "Автоматическая сериализация объектов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для сравнения объектов по значению?", + "answers": [ + "==", + "compareTo()", + "equals()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое StringBuilder?", + "answers": [ + "Неизменяемая строка", + "Изменяемая строка с синхронизацией", + "Изменяемая строка без синхронизации" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из этих типов коллекций не допускает дубликатов?", + "answers": [ + "List", + "Set", + "Queue" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое generics в Java?", + "answers": [ + "Механизм обработки исключений", + "Механизм обеспечения типобезопасности коллекций", + "Способ создания глобальных переменных" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод должен быть переопределён при использовании объекта в HashMap в качестве ключа?", + "answers": [ + "toString()", + "hashCode() и equals()", + "compareTo()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое класс-обёртка (wrapper class)?", + "answers": [ + "Класс, оборачивающий примитивный тип в объект", + "Класс, скрывающий реализацию другого класса", + "Класс, используемый только для тестирования" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов принадлежит классу Object?", + "answers": [ + "length()", + "clone()", + "size()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое композиция в ООП?", + "answers": [ + "Один класс наследуется от другого", + "Один класс содержит экземпляр другого класса как поле", + "Несколько классов реализуют один интерфейс" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой модификатор используется для запрета переопределения метода?", + "answers": [ + "static", + "final", + "abstract" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое статический блок в Java?", + "answers": [ + "Блок кода, выполняемый при каждом создании объекта", + "Блок кода, выполняемый один раз при загрузке класса", + "Блок кода внутри статического метода" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих типов не относится к коллекциям Java?", + "answers": [ + "ArrayList", + "HashMap", + "StringBuffer" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое NullPointerException?", + "answers": [ + "Исключение при попытке использовать null как объект", + "Ошибка компиляции из-за отсутствующего импорта", + "Исключение при выходе за границы массива" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих операторов имеет наивысший приоритет?", + "answers": [ + "+", + "*", + "()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое лямбда-выражение в Java?", + "answers": [ + "Анонимная функция, используемая с функциональными интерфейсами", + "Метод без параметров", + "Специальный тип исключения" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих интерфейсов функциональный?", + "answers": [ + "List", + "Runnable", + "Map" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое try-with-resources?", + "answers": [ + "Блок для обработки исключений без finally", + "Автоматическое закрытие ресурсов, реализующих AutoCloseable", + "Способ объявления глобальных переменных" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов не может быть статическим?", + "answers": [ + "main", + "toString", + "valueOf" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое наследование в Java?", + "answers": [ + "Создание нового класса на основе существующего", + "Использование нескольких интерфейсов", + "Объединение двух классов в один" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих типов данных занимает 64 бита?", + "answers": [ + "int", + "long", + "short" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что делает ключевое слово 'super'?", + "answers": [ + "Вызывает конструктор текущего класса", + "Ссылается на родительский класс", + "Останавливает выполнение метода" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов используется для запуска потока?", + "answers": [ + "run()", + "start()", + "execute()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое поток (Thread) в Java?", + "answers": [ + "Объект для работы с файлами", + "Независимый путь выполнения кода", + "Тип коллекции" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих классов является immutable?", + "answers": [ + "StringBuilder", + "String", + "ArrayList" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое интерфейс Comparable?", + "answers": [ + "Позволяет сравнивать объекты для сортировки", + "Позволяет сериализовать объект", + "Позволяет клонировать объект" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов вызывается сборщиком мусора перед удалением объекта?", + "answers": [ + "finalize()", + "destroy()", + "close()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое enum в Java?", + "answers": [ + "Специальный класс для определения констант", + "Тип исключения", + "Модификатор доступа" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов принадлежит классу Thread?", + "answers": [ + "sleep()", + "wait()", + "notify()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое synchronized в Java?", + "answers": [ + "Ключевое слово для управления доступом к ресурсам в многопоточной среде", + "Метод для остановки потока", + "Аннотация для тестов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих типов коллекций гарантирует порядок вставки?", + "answers": [ + "HashSet", + "LinkedHashSet", + "TreeSet" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое класс Class в Java?", + "answers": [ + "Метаописание типа во время выполнения", + "Родитель всех пользовательских классов", + "Класс для работы с файлами" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для получения длины массива?", + "answers": [ + "length()", + "size()", + "length" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое checked exception?", + "answers": [ + "Исключение, которое должно быть обработано или объявлено", + "Исключение времени выполнения", + "Ошибка компиляции" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов не принадлежит классу String?", + "answers": [ + "substring()", + "append()", + "charAt()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое var в Java 10+?", + "answers": [ + "Ключевое слово для объявления переменной с выводом типа", + "Тип данных для хранения любого значения", + "Синоним для Object" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих операторов используется для сравнения ссылок?", + "answers": [ + "equals()", + "==", + "compareTo()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое анонимный класс?", + "answers": [ + "Класс без имени, объявленный и инстанцированный в одном выражении", + "Класс, не имеющий конструктора", + "Класс, помеченный как private" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для ожидания завершения потока?", + "answers": [ + "join()", + "wait()", + "sleep()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое StackOverflowError?", + "answers": [ + "Ошибка при переполнении стека вызовов (например, бесконечная рекурсия)", + "Ошибка при нехватке памяти в куче", + "Ошибка при работе с сетью" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих типов не может быть ключом в HashMap?", + "answers": [ + "String", + "Integer", + "null" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое метод main объявлен как static?", + "answers": [ + "Чтобы его можно было вызвать без создания экземпляра класса", + "Чтобы он выполнялся в отдельном потоке", + "Чтобы он был недоступен из других классов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих классов реализует интерфейс List?", + "answers": [ + "HashSet", + "TreeMap", + "ArrayList" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое volatile переменная?", + "answers": [ + "Переменная, значение которой может измениться в другом потоке и всегда читается из памяти", + "Переменная, которая автоматически удаляется", + "Переменная, доступная только для чтения" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов принадлежит классу Math?", + "answers": [ + "random()", + "shuffle()", + "sort()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое класс-компаньон в Java?", + "answers": [ + "Такого понятия нет в Java (есть в Kotlin/Scala)", + "Статический внутренний класс", + "Класс с тем же именем, что и файл" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для преобразования строки в число?", + "answers": [ + "Integer.parseInt()", + "Integer.valueOf().intValue()", + "Оба предыдущих" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое serialVersionUID?", + "answers": [ + "Уникальный идентификатор для сериализуемых классов", + "Номер версии компилятора", + "Идентификатор потока" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов не является частью жизненного цикла потока?", + "answers": [ + "NEW", + "RUNNING", + "DESTROYED" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое shallow copy?", + "answers": [ + "Полное копирование объекта и всех вложенных объектов", + "Копирование только ссылок на вложенные объекты", + "Копирование только примитивных полей" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов используется для сериализации объекта?", + "answers": [ + "writeObject()", + "save()", + "serialize()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое класс ClassCastException?", + "answers": [ + "Исключение при неправильном приведении типов", + "Ошибка при работе с классами", + "Исключение при отсутствии класса в classpath" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для ожидания условия в многопоточности?", + "answers": [ + "wait()", + "sleep()", + "yield()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое интерфейс Iterator?", + "answers": [ + "Интерфейс для перебора элементов коллекции", + "Интерфейс для создания потоков", + "Интерфейс для обработки исключений" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов не модифицирует исходную коллекцию?", + "answers": [ + "Collections.sort()", + "list.add()", + "list.remove()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое default-метод в интерфейсе?", + "answers": [ + "Метод с реализацией по умолчанию, добавленный в Java 8", + "Метод, вызываемый по умолчанию при создании объекта", + "Метод, помеченный аннотацией @Default" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих классов является потокобезопасным?", + "answers": [ + "ArrayList", + "HashMap", + "ConcurrentHashMap" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое System.out.println()?", + "answers": [ + "Метод для записи в файл", + "Метод для вывода текста в стандартный поток вывода", + "Метод для логирования ошибок" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих операторов используется для логического И?", + "answers": [ + "&", + "&&", + "AND" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое boxing?", + "answers": [ + "Преобразование объекта в примитив", + "Преобразование примитива в объект", + "Упаковка нескольких переменных в массив" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов используется для сравнения строк без учёта регистра?", + "answers": [ + "equals()", + "equalsIgnoreCase()", + "compareToIgnoreCase()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое класс RuntimeException?", + "answers": [ + "Checked exception", + "Unchecked exception", + "Ошибка компиляции" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов возвращает текущий поток?", + "answers": [ + "Thread.currentThread()", + "Thread.getRunningThread()", + "Thread.activeThread()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое transient поле?", + "answers": [ + "Поле, игнорируемое при сериализации", + "Временная переменная в методе", + "Поле, доступное только в том же пакете" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для остановки потока (устаревший)?", + "answers": [ + "stop()", + "interrupt()", + "halt()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое instanceof?", + "answers": [ + "Оператор для проверки принадлежности объекта к типу", + "Метод для получения типа объекта", + "Аннотация для наследования" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих типов коллекций работает по принципу FIFO?", + "answers": [ + "Stack", + "Queue", + "Set" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое метод hashCode()?", + "answers": [ + "Возвращает уникальный идентификатор объекта", + "Возвращает целочисленное значение, используемое в хеш-таблицах", + "Возвращает адрес объекта в памяти" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих классов используется для работы с датой и временем в Java 8+?", + "answers": [ + "Date", + "Calendar", + "LocalDateTime" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое Stream API?", + "answers": [ + "API для работы с сетевыми потоками", + "API для функциональной обработки коллекций", + "API для работы с файлами" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих методов Stream терминальный?", + "answers": [ + "map()", + "filter()", + "collect()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое Optional в Java?", + "answers": [ + "Контейнер для значения, которое может быть null", + "Тип исключения", + "Специальный вид коллекции" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов создаёт параллельный Stream?", + "answers": [ + "stream()", + "parallelStream()", + "parallel()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое Predicate в Java?", + "answers": [ + "Функциональный интерфейс, принимающий аргумент и возвращающий boolean", + "Интерфейс для сравнения объектов", + "Аннотация для валидации" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для преобразования Stream в список?", + "answers": [ + "toList()", + "collect(Collectors.toList())", + "asList()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое модульность в Java 9+?", + "answers": [ + "Система управления зависимостями через pom.xml", + "Система разделения кода на модули с явными зависимостями", + "Новый способ объявления пакетов" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из этих операторов используется для тернарного условия?", + "answers": [ + "?:", + "??", + "if-else" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое record в Java 14+?", + "answers": [ + "Специальный тип класса для хранения неизменяемых данных", + "Тип исключения", + "Новый вид интерфейса" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для получения текущей даты?", + "answers": [ + "new Date()", + "LocalDate.now()", + "Calendar.getInstance().getDate()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое switch expression в Java 12+?", + "answers": [ + "Switch, возвращающий значение", + "Новый цикл", + "Альтернатива if-else" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов не может быть вызван у примитивного типа?", + "answers": [ + "toString()", + "intValue()", + "Ни один из них" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое класс AssertionError?", + "answers": [ + "Исключение, выбрасываемое при провале assert", + "Ошибка компиляции", + "Исключение при работе с файлами" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для замены подстроки?", + "answers": [ + "replace()", + "replaceAll()", + "Оба предыдущих" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что такое класс ClassLoader?", + "answers": [ + "Загружает классы в JVM во время выполнения", + "Компилирует Java-файлы", + "Управляет памятью" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для паузы в потоке?", + "answers": [ + "wait()", + "sleep()", + "pause()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что такое интерфейс Cloneable?", + "answers": [ + "Маркерный интерфейс для разрешения клонирования", + "Интерфейс с методом clone()", + "Интерфейс для копирования файлов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из этих методов используется для получения системного свойства?", + "answers": [ + "System.getProperty()", + "System.getenv()", + "Runtime.getProperty()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое OutOfMemoryError?", + "answers": [ + "Ошибка при нехватке памяти в куче", + "Ошибка при переполнении стека", + "Ошибка при работе с диском" + ], + "correctAnswerIndex": 0 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/java-syntax.json b/src/main/resources/questions/java-syntax.json new file mode 100644 index 0000000..6b61a20 --- /dev/null +++ b/src/main/resources/questions/java-syntax.json @@ -0,0 +1,812 @@ +[ + { + "questionText": "Какой из следующих вариантов является правильным объявлением метода main?", + "answers": [ + "public static void main(String[] args)", + "static public void start(String args[])", + "void Main(String[] arguments)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой символ используется для однострочного комментария в Java?", + "answers": [ + "//", + "/*", + "#" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из перечисленных типов не является примитивным в Java?", + "answers": [ + "boolean", + "char", + "String" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что будет результатом выражения: 10 / 3 при использовании целочисленного деления?", + "answers": [ + "3.333", + "3", + "4" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой оператор используется для логического ИЛИ?", + "answers": [ + "&", + "||", + "OR" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как правильно объявить константу в Java?", + "answers": [ + "const int MAX = 100;", + "final int MAX = 100;", + "static int MAX = 100;" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих вариантов НЕ является допустимым именем переменной?", + "answers": [ + "_value", + "2ndPlace", + "myVar" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой тип данных используется для хранения одиночного символа?", + "answers": [ + "String", + "char", + "Character" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой оператор используется для присваивания остатка от деления?", + "answers": [ + "/", + "%", + "\\" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих циклов гарантирует хотя бы одно выполнение тела?", + "answers": [ + "for", + "while", + "do-while" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой ключевой слово используется для немедленного выхода из цикла?", + "answers": [ + "stop", + "exit", + "break" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов имеет наивысший приоритет?", + "answers": [ + "+", + "==", + "*" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как правильно объявить массив целых чисел?", + "answers": [ + "int[] arr;", + "int arr[];", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что делает оператор '++' перед переменной (например, ++i)?", + "answers": [ + "Сначала возвращает значение, потом увеличивает", + "Сначала увеличивает, потом возвращает значение", + "Увеличивает значение на 2" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих литералов обозначает число с плавающей точкой?", + "answers": [ + "42", + "42L", + "42.0" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой тип данных может хранить значения true или false?", + "answers": [ + "bool", + "boolean", + "Boolean" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов проверяет равенство значений объектов?", + "answers": [ + "==", + "===", + "equals()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения ссылок на объекты?", + "answers": [ + "equals()", + "==", + "compareTo()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих вариантов — правильный способ объявить пакет?", + "answers": [ + "package com.example;", + "import com.example;", + "namespace com.example;" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для логического НЕ?", + "answers": [ + "!", + "not", + "~" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих вариантов вызовет ошибку компиляции?", + "answers": [ + "int x = 5;", + "byte b = 200;", + "double d = 3.14;" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих символов используется для многострочного комментария?", + "answers": [ + "// ... //", + "/* ... */", + "<!-- ... -->" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для побитового И?", + "answers": [ + "&&", + "&", + "||" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих литералов обозначает long-значение?", + "answers": [ + "100", + "100L", + "100.0" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для тернарного условия?", + "answers": [ + "if-else", + "?:", + "??" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих вариантов — правильное объявление класса?", + "answers": [ + "class MyClass { }", + "Class MyClass { }", + "new class MyClass { }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для инкремента после использования?", + "answers": [ + "i++", + "++i", + "i += 1" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих типов данных занимает 16 бит?", + "answers": [ + "byte", + "short", + "int" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для сдвига битов влево?", + "answers": [ + "<<", + ">>", + ">>>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для сдвига битов вправо с сохранением знака?", + "answers": [ + "<<", + ">>", + ">>>" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для сдвига битов вправо без сохранения знака?", + "answers": [ + ">>>", + ">>", + "<<" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих литералов представляет шестнадцатеричное число?", + "answers": [ + "0x1A", + "017", + "1A" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих литералов представляет восьмеричное число?", + "answers": [ + "017", + "0x17", + "17" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов имеет наименьший приоритет?", + "answers": [ + "=", + "+", + "*" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объединения строк?", + "answers": [ + "+", + "&", + "concat" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для присваивания и сложения?", + "answers": [ + "=+", + "+=", + "++=" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для присваивания и умножения?", + "answers": [ + "*=", + "=*", + "**=" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения неравенства?", + "answers": [ + "!=", + "<>", + "=/=" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения 'меньше или равно'?", + "answers": [ + "<=", + "=<", + "le" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения 'больше или равно'?", + "answers": [ + "=>", + ">=", + "ge" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для логического И с коротким замыканием?", + "answers": [ + "&", + "&&", + "and" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для логического ИЛИ с коротким замыканием?", + "answers": [ + "|", + "||", + "or" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для побитового исключающего ИЛИ?", + "answers": [ + "^", + "|", + "&" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для побитового НЕ?", + "answers": [ + "!", + "~", + "^" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для получения остатка от деления?", + "answers": [ + "/", + "\\", + "%" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов используется для доступа к члену объекта?", + "answers": [ + "->", + ".", + "::" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для доступа к статическому члену класса?", + "answers": [ + ".", + "::", + "->" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для создания нового объекта?", + "answers": [ + "create", + "new", + "make" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для приведения типов?", + "answers": [ + "(Type)", + "as Type", + "cast(Type)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для проверки типа объекта?", + "answers": [ + "is", + "instanceof", + "typeof" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для перехода к следующей итерации цикла?", + "answers": [ + "continue", + "next", + "skip" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для возврата значения из метода?", + "answers": [ + "exit", + "return", + "back" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для объявления блока кода?", + "answers": [ + "[ ]", + "{ }", + "( )" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для объявления условного блока?", + "answers": [ + "if (...) { }", + "when (...) { }", + "case (...) { }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для множественного выбора?", + "answers": [ + "if-else if", + "switch", + "match" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для обработки исключений?", + "answers": [ + "try-catch", + "handle-error", + "on-error" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для выброса исключения?", + "answers": [ + "throw", + "throws", + "raise" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется в сигнатуре метода для указания возможных исключений?", + "answers": [ + "throw", + "throws", + "try" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для импорта класса?", + "answers": [ + "include", + "import", + "using" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для объявления интерфейса?", + "answers": [ + "interface", + "protocol", + "contract" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления абстрактного класса?", + "answers": [ + "abstract class", + "virtual class", + "base class" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для наследования класса?", + "answers": [ + "extends", + "inherits", + "implements" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для реализации интерфейса?", + "answers": [ + "extends", + "implements", + "uses" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для обращения к родительскому классу?", + "answers": [ + "this", + "parent", + "super" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов используется для обращения к текущему объекту?", + "answers": [ + "self", + "this", + "current" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для объявления статического метода?", + "answers": [ + "static void method()", + "void static method()", + "method static void()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления финального метода?", + "answers": [ + "final void method()", + "void final method()", + "method final void()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления приватного метода?", + "answers": [ + "private void method()", + "void private method()", + "method private void()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления защищённого метода?", + "answers": [ + "protected void method()", + "void protected method()", + "method protected void()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления публичного метода?", + "answers": [ + "public void method()", + "void public method()", + "method public void()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления переменной уровня класса?", + "answers": [ + "вне метода, внутри класса", + "внутри метода main", + "в блоке try" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления локальной переменной?", + "answers": [ + "внутри метода", + "вне класса", + "в блоке import" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления параметра метода?", + "answers": [ + "в скобках после имени метода", + "перед именем метода", + "в теле метода" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления анонимного класса?", + "answers": [ + "new Interface() { }", + "class { }", + "anonymous class { }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления лямбда-выражения?", + "answers": [ + "(a) -> a * 2", + "lambda a: a * 2", + "function(a) { return a * 2; }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления var-переменной (Java 10+)?", + "answers": [ + "var x = 10;", + "auto x = 10;", + "let x = 10;" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для текстового блока (Java 15+)?", + "answers": [ + """"text"""", + "'''text'''", + "`text`" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для switch-выражения (Java 14+)?", + "answers": [ + "int result = switch(day) { case MON -> 1; default -> 0; };", + "switch(day) returns int { ... }", + "match(day) { ... }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления записи (record) в Java 14+?", + "answers": [ + "record Point(int x, int y) { }", + "data class Point(int x, int y)", + "struct Point { int x, y; }" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для yield в switch (Java 13+)?", + "answers": [ + "yield value;", + "return value;", + "break value;" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для assert-утверждения?", + "answers": [ + "assert condition;", + "check condition;", + "verify condition;" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для многоточия в параметрах метода (varargs)?", + "answers": [ + "void method(String... args)", + "void method(String[] args)", + "void method(String* args)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для инициализации массива при объявлении?", + "answers": [ + "int[] a = {1, 2, 3};", + "int[] a = new int[1, 2, 3];", + "int[] a = (1, 2, 3);" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для получения длины массива?", + "answers": [ + "array.length()", + "array.size()", + "array.length" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения двух строк по содержимому?", + "answers": [ + "str1 == str2", + "str1.equals(str2)", + "str1.compareTo(str2) == 0" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой из следующих операторов используется для сравнения двух строк без учёта регистра?", + "answers": [ + "str1.equalsIgnoreCase(str2)", + "str1.equals(str2, true)", + "str1.compareIgnoreCase(str2)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для преобразования строки в целое число?", + "answers": [ + "Integer.parseInt(str)", + "str.toInt()", + "int(str)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для преобразования числа в строку?", + "answers": [ + "String.valueOf(num)", + "num.toString()", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой из следующих операторов используется для объявления многомерного массива?", + "answers": [ + "int[][] matrix = new int[3][3];", + "int[3][3] matrix;", + "int matrix[3,3];" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой из следующих операторов используется для объявления перечисления (enum)?", + "answers": [ + "enum Color { RED, GREEN, BLUE }", + "enum Color = { RED, GREEN, BLUE };", + "enumeration Color { RED, GREEN, BLUE }" + ], + "correctAnswerIndex": 0 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/junit5.json b/src/main/resources/questions/junit5.json new file mode 100644 index 0000000..6ca616b --- /dev/null +++ b/src/main/resources/questions/junit5.json @@ -0,0 +1,524 @@ +[ + { + "questionText": "Какой модуль JUnit 5 отвечает за запуск тестов?", + "answers": [ + "JUnit Platform", + "JUnit Jupiter", + "JUnit Vintage" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой модуль предоставляет новые API для написания тестов в JUnit 5?", + "answers": [ + "JUnit Platform", + "JUnit Jupiter", + "JUnit Core" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой модуль позволяет запускать тесты JUnit 3 и 4 в JUnit 5?", + "answers": [ + "JUnit Legacy", + "JUnit Vintage", + "JUnit Compatibility Layer" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какая аннотация используется для пометки метода как тестового в JUnit 5?", + "answers": [ + "@org.junit.Test", + "@org.junit.jupiter.api.Test", + "@TestMethod" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой импорт необходим для использования @Test в JUnit 5?", + "answers": [ + "import org.junit.Test;", + "import org.junit.jupiter.api.Test;", + "import org.junit.platform.Test;" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается перед каждым тестом?", + "answers": [ + "@Before", + "@BeforeEach", + "@Setup" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается после каждого теста?", + "answers": [ + "@After", + "@AfterEach", + "@Cleanup" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается один раз перед всеми тестами в классе?", + "answers": [ + "@BeforeClass", + "@BeforeAll", + "@GlobalSetup" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается один раз после всех тестов в классе?", + "answers": [ + "@AfterClass", + "@AfterAll", + "@GlobalTeardown" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой модификатор доступа должен иметь тестовый метод в JUnit 5?", + "answers": [ + "public", + "package-private (default)", + "любой, кроме private" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой класс используется для стандартных утверждений (assertions) в JUnit 5?", + "answers": [ + "org.junit.Assert", + "org.junit.jupiter.api.Assertions", + "org.junit.jupiter.api.Assert" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как проверить, что два значения равны?", + "answers": [ + "assertEquals(expected, actual)", + "assertEqual(actual, expected)", + "verifyEquals(expected, actual)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, что условие истинно?", + "answers": [ + "assertTrue(condition)", + "assertCondition(condition)", + "verifyTrue(condition)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, что метод выбрасывает конкретное исключение?", + "answers": [ + "assertThrows(IOException.class, () -> method())", + "expectThrows(IOException.class, method())", + "assertException(IOException.class, method())" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для игнорирования теста?", + "answers": [ + "@Ignore", + "@Disabled", + "@Skip" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как добавить поясняющее сообщение к утверждению?", + "answers": [ + "assertEquals(expected, actual, "Values must be equal")", + "assertEquals("Values must be equal", expected, actual)", + "Оба способа допустимы" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как выполнить несколько утверждений, даже если одно из них падает?", + "answers": [ + "assertAll(() -> assertEquals(...), () -> assertTrue(...))", + "groupedAsserts(...)", + "softAssertions(...)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией помечают параметризованный тест?", + "answers": [ + "@TestWithParams", + "@ParameterizedTest", + "@DataDrivenTest" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой источник данных используется для передачи значений в @ParameterizedTest?", + "answers": [ + "@ValueSource", + "@DataSource", + "@TestInput" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как протестировать метод со строковыми аргументами через @ValueSource?", + "answers": [ + "@ValueSource(strings = {"a", "b"})", + "@ValueSource(values = {"a", "b"})", + "@StringSource({"a", "b"})" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно передать аргументы из CSV-строки?", + "answers": [ + "@CsvSource", + "@CsvData", + "@TableSource" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип аргумента может принимать метод с @MethodSource?", + "answers": [ + "Stream, Iterable, Iterator или Object[]", + "Только List<String>", + "Только статические массивы" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно задать имя теста в параметризованном тесте?", + "answers": [ + "name = "{0} => {1}" в @ParameterizedTest", + "@DisplayName", + "Нельзя — имя генерируется автоматически" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно сгруппировать тесты по логической категории?", + "answers": [ + "@Category", + "@Tag", + "@Group" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как запустить только тесты с определённым тегом через Maven?", + "answers": [ + "mvn test -Dgroups=fast", + "mvn test -DincludeTags=fast", + "mvn test -Dgroups не поддерживается в JUnit 5" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой аннотацией можно создать вложенный тестовый класс?", + "answers": [ + "@Nested", + "@InnerTest", + "@SubTest" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Может ли вложенный класс (@Nested) содержать @BeforeAll?", + "answers": [ + "Да, всегда", + "Нет, только если он static", + "Нет, @BeforeAll не поддерживается во вложенных классах" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой аннотацией можно повторить тест несколько раз?", + "answers": [ + "@Repeat(5)", + "@RepeatedTest(5)", + "@Test(repeat = 5)" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как получить информацию о текущем повторении в @RepeatedTest?", + "answers": [ + "Через параметр RepeatedTest.RepetitionInfo", + "Через системное свойство", + "Нельзя — информация недоступна" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс реализуется для создания пользовательского расширения (extension)?", + "answers": [ + "TestExtension", + "Extension", + "JUnitExtension" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как применить расширение ко всему классу?", + "answers": [ + "@ExtendWith(MyExtension.class)", + "@Extension(MyExtension.class)", + "@UseExtension(MyExtension.class)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой callback вызывается до выполнения тестового метода?", + "answers": [ + "beforeEach(ExtensionContext context)", + "preTest()", + "onTestStart()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой класс используется для работы с предположениями (assumptions)?", + "answers": [ + "Assumptions", + "Assume", + "Conditions" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что происходит, если assumption ложное?", + "answers": [ + "Тест помечается как failed", + "Тест пропускается (skipped)", + "Выбрасывается исключение" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как проверить, что значение не null?", + "answers": [ + "assertNotNull(value)", + "assertNotVoid(value)", + "verifyExists(value)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что два объекта ссылаются на один и тот же объект?", + "answers": [ + "assertSame(expected, actual)", + "assertReference(expected, actual)", + "assertIdentity(expected, actual)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что два объекта НЕ равны?", + "answers": [ + "assertNotEquals(a, b)", + "assertDifferent(a, b)", + "verifyNotEqual(a, b)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что условие ложно?", + "answers": [ + "assertFalse(condition)", + "assertNotTrue(condition)", + "verifyFalse(condition)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что массивы равны?", + "answers": [ + "assertArrayEquals(expected, actual)", + "assertEquals(expected, actual)", + "Оба метода работают одинаково" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что исключение не выбрасывается?", + "answers": [ + "assertDoesNotThrow(() -> method())", + "assertNoException(() -> method())", + "verifySafe(() -> method())" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно задать человекочитаемое имя теста?", + "answers": [ + "@Name", + "@DisplayName", + "@Title" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Поддерживает ли JUnit 5 тестирование приватных методов напрямую?", + "answers": [ + "Да, через Reflection API", + "Нет, но можно использовать рефлексию вручную", + "Да, через специальный assertPrivate()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой зависимостью нужно дополнить pom.xml для запуска JUnit 5 в Maven?", + "answers": [ + "junit-jupiter-engine", + "junit-platform-runner", + "Обе зависимости: junit-jupiter и junit-platform" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой плагин Maven необходим для запуска JUnit 5?", + "answers": [ + "maven-surefire-plugin версии 2.22.0+", + "maven-junit5-plugin", + "maven-test-plugin" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой класс используется для временных файлов и каталогов в тестах?", + "answers": [ + "@TempDir Path tempDir", + "@TemporaryFolder", + "TempFileSystem" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно протестировать производительность (время выполнения)?", + "answers": [ + "@Timeout", + "@TimedTest", + "Нет встроенной поддержки — только через assert" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как ограничить время выполнения теста?", + "answers": [ + "@Timeout(2) — 2 секунды", + "@Test(timeout = 2000)", + "Через System.currentTimeMillis()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип параметра можно использовать в @BeforeEach?", + "answers": [ + "Только void", + "Можно использовать ExtensionContext", + "Любой тип, зарегистрированный через расширение" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Можно ли использовать DI-подобные параметры в тестовых методах?", + "answers": [ + "Нет, только через рефлексию", + "Да, через механизмы расширений (например, MockitoExtension)", + "Только в SpringBootTest" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой аннотацией можно заменить несколько утверждений с отложенной проверкой?", + "answers": [ + "assertSoftly", + "assertAll", + "groupedAssertions" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для проверки, что строка содержит подстроку?", + "answers": [ + "assertTrue(str.contains("sub"))", + "assertThat(str).contains("sub")", + "Нет встроенного метода — только через assertTrue" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Поддерживает ли JUnit 5 встроенные matchers (как Hamcrest)?", + "answers": [ + "Да, через Assertions.assertThat()", + "Нет, но можно подключить Hamcrest отдельно", + "Только в JUnit 4" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой интерфейс нужно реализовать для условного запуска теста?", + "answers": [ + "ExecutionCondition", + "TestCondition", + "ConditionalExtension" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно запустить тест только в определённой ОС?", + "answers": [ + "@EnabledOnOs(OS.WINDOWS)", + "@RunIfOs(WINDOWS)", + "Нет такой аннотации — только через assume" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно отключить тест на определённой JDK?", + "answers": [ + "@DisabledOnJre(JRE.JAVA_8)", + "@SkipIfJava8", + "@ExcludeJdk(8)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой класс используется для регистрации колбэков вручную (например, для очистки)?", + "answers": [ + "TestWatcher", + "AutoCloseable", + "ExtensionContext.Store.CloseableResource" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для проверки, что коллекция не пуста?", + "answers": [ + "assertFalse(collection.isEmpty())", + "assertNotEmpty(collection)", + "Нет встроенного метода — только через assertFalse" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой минимальный Java-версией требуется для JUnit 5?", + "answers": [ + "Java 7", + "Java 8", + "Java 11" + ], + "correctAnswerIndex": 1 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/logging.json b/src/main/resources/questions/logging.json new file mode 100644 index 0000000..c6fdf54 --- /dev/null +++ b/src/main/resources/questions/logging.json @@ -0,0 +1,515 @@ +[ + { + "questionText": "Какая библиотека является рекомендуемым фасадом для логирования в современных Java-приложениях?", + "answers": [ + "SLF4J", + "java.util.logging", + "Log4j 1.x" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что означает аббревиатура SLF4J?", + "answers": [ + "Simple Logging Framework for Java", + "Standard Logging Format for Java", + "Simple Logging Facade for Java" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой уровень логирования используется для отладочной информации?", + "answers": [ + "INFO", + "DEBUG", + "TRACE" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой уровень логирования самый высокий по приоритету?", + "answers": [ + "FATAL", + "ERROR", + "WARN" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как правильно записать параметризованное сообщение в SLF4J?", + "answers": [ + "logger.info("User {} logged in", username)", + "logger.info("User " + username + " logged in")", + "logger.info("User %s logged in", username)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Почему предпочтительно использовать параметризацию вместо конкатенации строк в логах?", + "answers": [ + "Потому что это красивее", + "Потому что строка не создаётся, если уровень логирования отключён", + "Потому что конкатенация запрещена в логах" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой фреймворк является нативной реализацией SLF4J?", + "answers": [ + "Log4j 2", + "Logback", + "java.util.logging" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой файл используется для конфигурации Logback по умолчанию?", + "answers": [ + "logback.xml", + "logging.properties", + "log4j2.xml" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой файл используется для конфигурации Log4j 2?", + "answers": [ + "log4j2.xml", + "log4j.xml", + "logging.xml" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как включить асинхронное логирование в Logback?", + "answers": [ + "Использовать AsyncAppender", + "Установить флаг async=true в конфигурации", + "Logback не поддерживает асинхронное логирование" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как включить асинхронное логирование в Log4j 2?", + "answers": [ + "Использовать AsyncLogger", + "Добавить зависимость log4j-async", + "Обернуть Appender в AsyncAppender" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования ниже DEBUG?", + "answers": [ + "TRACE", + "FINE", + "VERBOSE" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, включён ли уровень DEBUG перед дорогостоящей операцией?", + "answers": [ + "if (logger.isDebugEnabled()) { ... }", + "Это не нужно — SLF4J делает это автоматически при параметризации", + "Нужно всегда выполнять операцию" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой класс используется для получения логгера в SLF4J?", + "answers": [ + "LoggerFactory.getLogger(MyClass.class)", + "Logger.getLogger(MyClass.class)", + "SLF4J.getLogger(MyClass.class)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое MDC в контексте логирования?", + "answers": [ + "Mapped Diagnostic Context — карта для хранения диагностических данных на поток", + "Main Debug Console", + "Multi-threaded Data Collector" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как добавить значение в MDC?", + "answers": [ + "MDC.put("userId", userId)", + "logger.setContext("userId", userId)", + "ThreadLocal.set("userId", userId)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как использовать MDC в шаблоне лога (например, в logback.xml)?", + "answers": [ + "%X{userId}", + "${userId}", + "#{userId}" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой зависимости не хватает, если вы видите ошибку 'Failed to load class org.slf4j.impl.StaticLoggerBinder'?", + "answers": [ + "slf4j-api", + "Реализация SLF4J (например, logback-classic)", + "log4j-over-slf4j" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как перенаправить java.util.logging (JUL) в SLF4J?", + "answers": [ + "Использовать jul-to-slf4j", + "Это невозможно", + "Заменить все вызовы JUL на SLF4J вручную" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как перенаправить Log4j 1.x в SLF4J?", + "answers": [ + "Использовать log4j-over-slf4j", + "Использовать slf4j-log4j12", + "Нельзя — только ручная замена" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой артефакт используется для моста от SLF4J к Log4j 2?", + "answers": [ + "slf4j-log4j2", + "log4j-slf4j-impl", + "Это не требуется — Log4j 2 сам по себе" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой уровень логирования используется для информационных сообщений о работе приложения?", + "answers": [ + "DEBUG", + "INFO", + "NOTICE" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой уровень логирования используется для потенциально проблемных ситуаций?", + "answers": [ + "ERROR", + "WARN", + "ALERT" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой уровень логирования используется для ошибок, требующих вмешательства?", + "answers": [ + "FATAL", + "ERROR", + "CRITICAL" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой формат файла НЕ поддерживается Logback для конфигурации?", + "answers": [ + "logback.xml", + "logback.properties", + "logback.yaml" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как задать уровень логирования для конкретного пакета в logback.xml?", + "answers": [ + "<logger name="com.example" level="DEBUG"/>", + "<package name="com.example" level="DEBUG"/>", + "<level package="com.example" value="DEBUG"/>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент в logback.xml определяет вывод в консоль?", + "answers": [ + "<ConsoleAppender>", + "<StdOutAppender>", + "<TerminalAppender>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент в log4j2.xml определяет вывод в файл?", + "answers": [ + "<File>", + "<RollingFile>", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой шаблон (%conversion pattern) в Logback выводит имя класса?", + "answers": [ + "%c", + "%class", + "%logger" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой шаблон в Logback выводит имя метода?", + "answers": [ + "%M", + "%method", + "Logback не поддерживает вывод имени метода по соображениям производительности" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой шаблон в Logback выводит уровень логирования?", + "answers": [ + "%p", + "%level", + "%l" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой шаблон в Logback выводит временну́ю метку?", + "answers": [ + "%d", + "%date", + "%t" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой шаблон в Logback выводит ID потока?", + "answers": [ + "%thread", + "%t", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как избежать утечки памяти при использовании MDC в пуле потоков?", + "answers": [ + "Очистить MDC в finally-блоке или через interceptor", + "MDC автоматически очищается", + "Не использовать MDC в многопоточных приложениях" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования отключает все сообщения?", + "answers": [ + "OFF", + "NONE", + "SILENT" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования включает все сообщения?", + "answers": [ + "ALL", + "TRACE", + "VERBOSE" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой зависимости достаточно для использования SLF4J с Logback?", + "answers": [ + "slf4j-api и logback-classic", + "Только logback-classic", + "slf4j-api, logback-core и logback-classic" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой класс в Log4j 2 используется для получения логгера?", + "answers": [ + "LoggerFactory.getLogger()", + "LogManager.getLogger()", + "Logger.getLogger()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой формат конфигурации Log4j 2 НЕ поддерживается?", + "answers": [ + "XML", + "JSON", + "YAML (без дополнительных зависимостей)" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой механизм позволяет динамически менять уровень логирования без перезапуска приложения?", + "answers": [ + "JMX", + "Logback scan="true" в корневом элементе", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой элемент в logback.xml включает сканирование изменений конфигурации?", + "answers": [ + "<configuration scan="true">", + "<scan enabled="true"/>", + "Нельзя — только перезапуск" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс реализует логгер в SLF4J?", + "answers": [ + "org.slf4j.Logger", + "java.util.logging.Logger", + "ch.qos.logback.Logger" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Можно ли использовать несколько реализаций SLF4J одновременно?", + "answers": [ + "Да, без проблем", + "Нет, будет конфликт зависимостей", + "Только если они в разных classloader’ах" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой артефакт используется для перенаправления JUL в SLF4J?", + "answers": [ + "jul-to-slf4j", + "slf4j-jdk14", + "java-logging-bridge" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для логирования исключения в SLF4J?", + "answers": [ + "logger.error("Message", exception)", + "logger.log(exception)", + "logger.exception(exception)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования соответствует FINE в java.util.logging?", + "answers": [ + "DEBUG", + "TRACE", + "INFO" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования соответствует FINER в JUL?", + "answers": [ + "DEBUG", + "TRACE", + "VERBOSE" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой подход к логированию считается антипаттерном?", + "answers": [ + "Логирование в catch-блоке без rethrow", + "Использование INFO для старта приложения", + "Параметризация сообщений" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой формат логов рекомендуется для микросервисов?", + "answers": [ + "Человекочитаемый текст", + "JSON", + "XML" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как в Logback настроить ротацию логов по размеру и времени?", + "answers": [ + "Использовать RollingFileAppender с TimeBasedRollingPolicy и SizeBasedTriggeringPolicy", + "Logback не поддерживает комбинированную ротацию", + "Только через внешние утилиты (logrotate)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент в log4j2.xml задаёт политику ротации?", + "answers": [ + "<Policies>", + "<Rotation>", + "<TriggeringPolicy>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой плагин Log4j 2 позволяет фильтровать логи по регулярному выражению?", + "answers": [ + "RegexFilter", + "PatternFilter", + "Log4j 2 не поддерживает фильтрацию по regex" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой уровень логирования следует использовать в production по умолчанию?", + "answers": [ + "DEBUG", + "INFO", + "WARN" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как избежать логирования чувствительных данных (паролей, токенов)?", + "answers": [ + "Никогда не логировать такие данные", + "Использовать маскировку через MDC", + "Фильтровать логи на уровне Appender’а" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой механизм позволяет добавлять контекст (например, request ID) ко всем логам в веб-приложении?", + "answers": [ + "MDC + Servlet Filter", + "ThreadLocal + Interceptor", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой минимальный Java-версией требуется для Log4j 2.17+ (после уязвимости CVE-2021-44228)?", + "answers": [ + "Java 8", + "Java 11", + "Java 17" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой артефакт Log4j 2 обеспечивает интеграцию с SLF4J?", + "answers": [ + "log4j-slf4j-impl", + "slf4j-log4j2", + "log4j-to-slf4j" + ], + "correctAnswerIndex": 0 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/maven.json b/src/main/resources/questions/maven.json new file mode 100644 index 0000000..f14492c --- /dev/null +++ b/src/main/resources/questions/maven.json @@ -0,0 +1,542 @@ +[ + { + "questionText": "Какая команда используется для создания нового Maven-проекта из архетипа?", + "answers": [ + "mvn archetype:generate", + "mvn new-project", + "mvn init" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой файл является основным конфигурационным файлом Maven-проекта?", + "answers": [ + "build.xml", + "pom.xml", + "maven.config" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что означает аббревиатура POM?", + "answers": [ + "Project Object Model", + "Package Of Modules", + "Program Output Manager" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какая команда компилирует исходный код проекта?", + "answers": [ + "mvn compile", + "mvn build", + "mvn make" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какая команда запускает тесты?", + "answers": [ + "mvn test", + "mvn run-tests", + "mvn verify" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какая команда упаковывает проект в JAR/WAR?", + "answers": [ + "mvn package", + "mvn assemble", + "mvn deploy" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какая команда устанавливает артефакт в локальный репозиторий?", + "answers": [ + "mvn install", + "mvn publish-local", + "mvn cache" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какая команда загружает артефакт в удалённый репозиторий?", + "answers": [ + "mvn upload", + "mvn deploy", + "mvn release" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой scope зависимости используется по умолчанию?", + "answers": [ + "runtime", + "compile", + "provided" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой scope указывает, что зависимость нужна только для компиляции, но не включается в runtime?", + "answers": [ + "test", + "provided", + "system" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой scope используется для зависимостей, необходимых только при запуске тестов?", + "answers": [ + "compile", + "test", + "runtime" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой элемент в pom.xml определяет уникальный идентификатор проекта?", + "answers": [ + "<artifactId>", + "<groupId>", + "Комбинация <groupId>, <artifactId> и <version>" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Где хранятся скачанные зависимости по умолчанию?", + "answers": [ + "В папке .m2/repository в домашней директории пользователя", + "В папке target/lib проекта", + "В папке /usr/local/maven/repo" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент pom.xml задаёт версию Java для компиляции?", + "answers": [ + "<java.version>", + "Свойства maven.compiler.source и maven.compiler.target", + "<sourceVersion>" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой плагин отвечает за компиляцию исходного кода?", + "answers": [ + "maven-jar-plugin", + "maven-compiler-plugin", + "maven-surefire-plugin" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой плагин запускает unit-тесты?", + "answers": [ + "maven-failsafe-plugin", + "maven-surefire-plugin", + "maven-test-plugin" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой плагин используется для создания исполняемого JAR-файла?", + "answers": [ + "maven-shade-plugin или maven-assembly-plugin", + "maven-exec-plugin", + "maven-jar-plugin напрямую" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как указать основной класс (mainClass) в maven-jar-plugin?", + "answers": [ + "Через <Main-Class> в <manifest>", + "Через <mainClass> в конфигурации плагина", + "Оба способа возможны в разных плагинах" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой элемент pom.xml используется для наследования конфигурации от родительского POM?", + "answers": [ + "<parent>", + "<extends>", + "<superPom>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как создать multi-module проект?", + "answers": [ + "Добавить <modules> с перечислением подмодулей в родительский pom.xml", + "Создать папки и указать их в .mvn/modules.txt", + "Использовать mvn module:create" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой файл можно использовать для глобальной настройки Maven (например, зеркал)?", + "answers": [ + "settings.xml в ~/.m2/", + "pom.xml в корне проекта", + "maven.config в корне проекта" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как активировать профиль (profile) из командной строки?", + "answers": [ + "mvn -PmyProfile", + "mvn --profile myProfile", + "mvn -Dprofile=myProfile" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Где можно определить профиль (profile)?", + "answers": [ + "Только в pom.xml", + "В pom.xml или settings.xml", + "Только в settings.xml" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой элемент pom.xml позволяет управлять версиями зависимостей централизованно?", + "answers": [ + "<dependencyManagement>", + "<dependencies>", + "<versionControl>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Чем отличается <dependencyManagement> от <dependencies>?", + "answers": [ + "<dependencyManagement> задаёт версии, но не добавляет зависимости; <dependencies> — добавляет", + "Нет разницы — это синонимы", + "<dependencies> только для тестов, <dependencyManagement> — для основного кода" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как пропустить тесты при сборке?", + "answers": [ + "mvn package -DskipTests", + "mvn package -Dmaven.test.skip=true", + "Оба варианта, но они работают по-разному" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что делает флаг -Dmaven.test.skip=true?", + "answers": [ + "Пропускает запуск тестов, но компилирует их", + "Пропускает и компиляцию, и запуск тестов", + "Отключает только вывод результатов" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой фазе жизненного цикла предшествует фаза 'package'?", + "answers": [ + "compile", + "test", + "verify" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какая фаза жизненного цикла запускается последней при выполнении 'mvn install'?", + "answers": [ + "package", + "install", + "deploy" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как очистить скомпилированные файлы и папку target?", + "answers": [ + "mvn clean", + "mvn reset", + "mvn erase" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент pom.xml задаёт тип артефакта (jar, war, pom)?", + "answers": [ + "<packaging>", + "<type>", + "<artifactType>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой packaging используется для multi-module родительского проекта?", + "answers": [ + "jar", + "pom", + "multi" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как указать собственный репозиторий для зависимости?", + "answers": [ + "Нельзя — только через settings.xml или mirror", + "Через <repository> в pom.xml", + "Через <url> внутри <dependency>" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как обновить снепшот-зависимости принудительно?", + "answers": [ + "mvn -U", + "mvn --refresh", + "mvn update-dependencies" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как посмотреть дерево зависимостей?", + "answers": [ + "mvn dependency:tree", + "mvn list-dependencies", + "mvn deps" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как исключить транзитивную зависимость?", + "answers": [ + "Через <exclusions> внутри <dependency>", + "Удалить её из локального репозитория", + "Использовать scope 'none'" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент pom.xml используется для задания свойств (properties)?", + "answers": [ + "<properties>", + "<vars>", + "<config>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как использовать свойство в pom.xml?", + "answers": [ + "${my.property}", + "#{my.property}", + "%my.property%" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой плагин используется для запуска Java-приложения из Maven?", + "answers": [ + "maven-exec-plugin", + "maven-run-plugin", + "maven-launcher-plugin" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как указать аргументы JVM при запуске через exec:java?", + "answers": [ + "Нельзя — только через системные свойства", + "Через <arguments> в конфигурации", + "Через системное свойство MAVEN_OPTS" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой элемент pom.xml позволяет задать описание проекта?", + "answers": [ + "<description>", + "<info>", + "<summary>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент указывает URL домашней страницы проекта?", + "answers": [ + "<url>", + "<homepage>", + "<projectUrl>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой файл содержит пользовательские настройки Maven (например, credentials)?", + "answers": [ + "~/.m2/settings.xml", + "pom.xml", + "mvn-settings.xml" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как задать зеркало (mirror) для центрального репозитория?", + "answers": [ + "В settings.xml через <mirrors>", + "В pom.xml через <mirrorRepository>", + "Через системную переменную MIRROR_URL" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой scope используется для зависимостей, предоставляемых средой выполнения (например, servlet-api в Tomcat)?", + "answers": [ + "runtime", + "provided", + "system" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой scope НЕ транзитивен?", + "answers": [ + "compile", + "test", + "runtime" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как создать JAR с зависимостями внутри?", + "answers": [ + "Использовать maven-shade-plugin", + "Использовать maven-assembly-plugin", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой плагин генерирует site-документацию проекта?", + "answers": [ + "maven-site-plugin", + "maven-docs-plugin", + "maven-report-plugin" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить наличие уязвимостей в зависимостях?", + "answers": [ + "mvn dependency:analyze", + "mvn org.owasp:dependency-check-maven:check", + "Встроенной команды нет; требуется внешний плагин" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой элемент pom.xml используется для указания лицензии проекта?", + "answers": [ + "<license>", + "<licenses>", + "<legal>" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как указать список разработчиков в pom.xml?", + "answers": [ + "<developers>", + "<team>", + "<contributors>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой командой можно вывести версию Maven?", + "answers": [ + "mvn --version", + "mvn -v", + "Обе команды верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой файл может содержать опции JVM для Maven (например, -Xmx)?", + "answers": [ + ".mvn/jvm.config", + "maven.config", + "mvn.opts" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как игнорировать файлы в Maven-сборке (аналог .gitignore)?", + "answers": [ + "Maven не имеет такого механизма — используйте .gitignore", + "Через <excludes> в плагинах (например, assembly)", + "Файл .mvnignore" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой элемент pom.xml задаёт версию проекта?", + "answers": [ + "<version>", + "<projectVersion>", + "<release>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как автоматизировать изменение версии проекта?", + "answers": [ + "mvn versions:set -DnewVersion=1.2.0", + "mvn release:update-version", + "Редактировать pom.xml вручную" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой плагин используется для управления версиями зависимостей?", + "answers": [ + "versions-maven-plugin", + "maven-version-plugin", + "dependency-updater-plugin" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, есть ли обновления для зависимостей?", + "answers": [ + "mvn versions:display-dependency-updates", + "mvn dependency:check-updates", + "mvn update:dependencies" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент pom.xml позволяет задать информацию о системе контроля версий?", + "answers": [ + "<scm>", + "<vcs>", + "<repository>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент используется для настройки distributionManagement (например, для deploy)?", + "answers": [ + "<distributionManagement>", + "<deployment>", + "<publishing>" + ], + "correctAnswerIndex": 0 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/mockito.json b/src/main/resources/questions/mockito.json new file mode 100644 index 0000000..19caa40 --- /dev/null +++ b/src/main/resources/questions/mockito.json @@ -0,0 +1,497 @@ +[ + { + "questionText": "Какой метод используется для создания мока в Mockito?", + "answers": [ + "Mockito.mock(MyClass.class)", + "Mockito.createMock(MyClass.class)", + "new Mock<>(MyClass.class)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией помечают поле для автоматического создания мока в JUnit 5?", + "answers": [ + "@MockBean", + "@Mock", + "@Stub" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой класс предоставляет статические методы Mockito (например, when, verify)?", + "answers": [ + "Mockito", + "Mock", + "MockitoCore" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить возвращаемое значение метода мока?", + "answers": [ + "when(mock.method()).thenReturn(value)", + "mock.method().thenReturn(value)", + "doReturn(value).when(mock.method())" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, что метод был вызван один раз?", + "answers": [ + "verify(mock).method()", + "assertCalledOnce(mock.method())", + "check(mock.method()).called()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией активируется поддержка Mockito в JUnit 5?", + "answers": [ + "@ExtendWith(MockitoExtension.class)", + "@RunWith(MockitoJUnitRunner.class)", + "@EnableMockito" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что такое spy в Mockito?", + "answers": [ + "Полная замена объекта на мок", + "Частичный мок: реальные методы вызываются, если не задано иное", + "Инструмент для перехвата исключений" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как создать spy объекта?", + "answers": [ + "Mockito.spy(realObject)", + "Mockito.mock(realObject, Spy.class)", + "new Spy(realObject)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить поведение void-метода мока?", + "answers": [ + "when(mock.voidMethod()).thenThrow(...)", + "doThrow(...).when(mock).voidMethod()", + "mock.voidMethod().toThrow(...)" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для проверки, что метод НЕ был вызван?", + "answers": [ + "verify(mock, never()).method()", + "verifyNever(mock.method())", + "assertNotCalled(mock.method())" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, что метод вызывался ровно 3 раза?", + "answers": [ + "verify(mock, times(3)).method()", + "verify(mock).method().times(3)", + "assertCallCount(mock.method(), 3)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой matcher используется для любого значения типа String?", + "answers": [ + "anyString()", + "any(String.class)", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой matcher используется для любого объекта?", + "answers": [ + "any()", + "anyObject()", + "all()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как зафиксировать аргумент, переданный в метод мока?", + "answers": [ + "Использовать ArgumentCaptor", + "Использовать any() и сохранить в переменную", + "Нельзя — Mockito не поддерживает захват аргументов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как создать ArgumentCaptor для типа String?", + "answers": [ + "ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class)", + "ArgumentCaptor<String> captor = new ArgumentCaptor<String>()", + "ArgumentCaptor.of(String.class)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как использовать ArgumentCaptor для проверки аргумента?", + "answers": [ + "verify(mock).method(captor.capture()); assertEquals("value", captor.getValue())", + "captor.capture(mock.method()); ...", + "mock.method(captor.get()); ..." + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой аннотацией можно автоматически внедрить моки в тестируемый объект?", + "answers": [ + "@AutowiredMocks", + "@InjectMocks", + "@WireMocks" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Что произойдёт, если не вызвать verify после настройки мока?", + "answers": [ + "Тест упадёт", + "Ничего — verify не обязателен", + "Мок будет считаться неиспользованным" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как сбросить все вызовы и настройки мока?", + "answers": [ + "Mockito.reset(mock)", + "mock.clear()", + "Mockito.clearInvocations(mock)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить порядок вызовов нескольких моков?", + "answers": [ + "Использовать InOrder: InOrder inOrder = inOrder(mock1, mock2); inOrder.verify(...)", + "Mockito не поддерживает проверку порядка", + "verify(mock1).method(); verify(mock2).method(); — порядок гарантируется" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить мок на выброс исключения?", + "answers": [ + "when(mock.method()).thenThrow(new RuntimeException())", + "doThrow(new RuntimeException()).when(mock).method()", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Можно ли мокировать final-классы в Mockito?", + "answers": [ + "Нет, никогда", + "Да, начиная с Mockito 2+ при использовании inline-mock-maker", + "Только если они реализуют интерфейс" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой зависимостью нужно дополнить проект для мокирования final-классов?", + "answers": [ + "mockito-core достаточно", + "Нужно добавить mockito-inline", + "Нужно использовать PowerMock" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как проверить, что метод вызывался как минимум 2 раза?", + "answers": [ + "verify(mock, atLeast(2)).method()", + "verify(mock).method().atLeast(2)", + "assertMinCalls(mock.method(), 2)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как проверить, что метод вызывался не более 3 раз?", + "answers": [ + "verify(mock, atMost(3)).method()", + "verify(mock, max(3)).method()", + "verify(mock).method().atMost(3)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить последовательность возвращаемых значений?", + "answers": [ + "when(mock.method()).thenReturn(a).thenReturn(b)", + "when(mock.method()).thenReturn(a, b)", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как настроить ответ, который вычисляется динамически на основе аргументов?", + "answers": [ + "when(mock.method(any())).thenAnswer(invocation -> { ... })", + "doAnswer(...).when(mock).method()", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для проверки, что мок не имеет непроверенных вызовов?", + "answers": [ + "Mockito.verifyNoMoreInteractions(mock)", + "Mockito.checkUnused(mock)", + "verify(mock).noMoreInteractions()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что делает verifyNoInteractions(mock)?", + "answers": [ + "Проверяет, что с моком вообще не взаимодействовали", + "Сбрасывает все вызовы", + "Отключает мок" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как мокировать статический метод в Mockito?", + "answers": [ + "Нельзя — только через PowerMock", + "Можно с Mockito 3.4.0+ и mockito-inline: try (MockedStatic<MyClass> mocked = mockStatic(MyClass.class))", + "Статические методы всегда вызываются реально" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для частичного мокирования (stubbing одного метода, остальные — реальные)?", + "answers": [ + "mock(class, CALLS_REAL_METHODS)", + "spy(object)", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Что произойдёт, если вызвать verify на spy без предварительного вызова метода?", + "answers": [ + "Будет ошибка — метод не вызывался", + "Ничего — verify пройдёт, если метод не вызывался", + "Spy нельзя проверять через verify" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как игнорировать исключение при вызове метода мока?", + "answers": [ + "Нельзя — исключение всегда пробрасывается", + "Настроить thenAnswer, который перехватывает исключение", + "Использовать doNothing().when(mock).voidMethod()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой matcher используется для проверки, что аргумент не null?", + "answers": [ + "notNull()", + "anyNonNull()", + "exists()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой matcher используется для проверки по условию?", + "answers": [ + "argThat(argument -> argument > 0)", + "matchCondition(arg -> arg > 0)", + "filter(arg -> arg > 0)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Можно ли использовать matchers и конкретные значения в одном вызове?", + "answers": [ + "Да, без ограничений", + "Нет, все аргументы должны быть matchers или все — значения", + "Только если значения обернуть в eq()" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как правильно смешать конкретное значение и matcher?", + "answers": [ + "when(mock.method(eq("value"), anyInt()))", + "when(mock.method("value", anyInt()))", + "Нельзя — только все matchers" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для мокирования конструктора?", + "answers": [ + "Mockito не поддерживает мокирование конструкторов", + "Использовать mockConstruction() (Mockito 4.6+)", + "Только через PowerMock" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как проверить, что метод вызывался с любым аргументом?", + "answers": [ + "verify(mock).method(any())", + "verify(mock).method(*)", + "verify(mock).method()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип возвращается по умолчанию для мокированного метода, возвращающего int?", + "answers": [ + "0", + "1", + "-1" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип возвращается по умолчанию для мокированного метода, возвращающего объект?", + "answers": [ + "null", + "пустой мок", + "исключение" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить мок на вызов реального метода при определённых условиях?", + "answers": [ + "when(mock.method()).thenCallRealMethod()", + "doCallRealMethod().when(mock).method()", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой аннотацией можно инициализировать поля с @Mock вручную?", + "answers": [ + "MockitoAnnotations.openMocks(this)", + "Mockito.initMocks(this)", + "Оба метода, но openMocks — новее" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для проверки вызова приватного метода?", + "answers": [ + "Mockito не поддерживает мокирование приватных методов напрямую", + "Использовать spy и рефлексию", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как мокировать enum?", + "answers": [ + "Нельзя — enum нельзя мокировать", + "Можно через mock(Enum.class), но с ограничениями", + "Только если enum реализует интерфейс" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки вызова приватного метода?", + "answers": [ + "InOrder.verify()", + "verify(mock, inOrder(otherMock)).method()", + "Mockito не поддерживает межмоковый порядок" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как отключить предупреждение 'Unnecessary stubbings'?", + "answers": [ + "Использовать lenient() при настройке: lenient().when(...).thenReturn(...)", + "Отключить в настройках Mockito", + "Удалить лишние when()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для создания мока с пользовательским поведением по умолчанию?", + "answers": [ + "mock(MyClass.class, Answers.RETURNS_SMART_NULLS)", + "mock(MyClass.class, new CustomAnswer())", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как проверить, что метод вызывался с точно таким же объектом (по ссылке)?", + "answers": [ + "verify(mock).method(same(expectedObject))", + "verify(mock).method(expectedObject)", + "Нельзя — Mockito всегда использует equals()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, что метод вызывался с экземпляром определённого класса?", + "answers": [ + "verify(mock).method(isA(MyClass.class))", + "verify(mock).method(instanceOf(MyClass.class))", + "Оба варианта верны" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как мокировать метод, который возвращает Optional?", + "answers": [ + "when(mock.method()).thenReturn(Optional.of(value))", + "Mockito обрабатывает Optional как обычный объект", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для проверки количества вызовов без указания метода?", + "answers": [ + "Mockito не позволяет проверять вызовы без указания метода", + "verify(mock, times(n))", + "Нужно использовать ArgumentCaptor" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Можно ли использовать Mockito для мокирования лямбда-выражений?", + "answers": [ + "Нет, лямбды — final", + "Да, если они реализуют функциональный интерфейс", + "Только через spy" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для проверки, что коллбэк был вызван с определённым аргументом?", + "answers": [ + "Использовать ArgumentCaptor на интерфейсе коллбэка", + "Нельзя — коллбэки не мокируются", + "Проверить через реальный вызов" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой минимальный Java-версией требуется для Mockito 5.x?", + "answers": [ + "Java 8", + "Java 11", + "Java 17" + ], + "correctAnswerIndex": 1 + } +] \ No newline at end of file diff --git a/src/main/resources/questions/servlets.json b/src/main/resources/questions/servlets.json new file mode 100644 index 0000000..af43408 --- /dev/null +++ b/src/main/resources/questions/servlets.json @@ -0,0 +1,857 @@ +[ + { + "questionText": "Какой интерфейс должен реализовывать базовый сервлет?", + "answers": [ + "javax.servlet.Servlet", + "javax.servlet.http.HttpServlet", + "java.net.Servlet" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой класс обычно наследуется при создании HTTP-сервлета?", + "answers": [ + "GenericServlet", + "HttpServlet", + "ServletBase" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод вызывается один раз при инициализации сервлета?", + "answers": [ + "init()", + "start()", + "configure()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод вызывается при каждом HTTP-запросе?", + "answers": [ + "execute()", + "service()", + "handleRequest()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод HttpServlet обрабатывает GET-запросы?", + "answers": [ + "doGet()", + "get()", + "handleGet()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод HttpServlet обрабатывает POST-запросы?", + "answers": [ + "doPost()", + "post()", + "handlePost()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой объект используется для получения параметров запроса?", + "answers": [ + "HttpServletResponse", + "HttpServletRequest", + "ServletContext" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как получить значение параметра 'name' из запроса?", + "answers": [ + "request.getParameter("name")", + "request.getQuery("name")", + "request.getAttribute("name")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Что возвращает метод getParameter() при отсутствии параметра?", + "answers": [ + "пустую строку", + "null", + "исключение" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой объект используется для отправки ответа клиенту?", + "answers": [ + "HttpServletRequest", + "HttpServletResponse", + "ServletOutputStream" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Как установить статус ответа 404?", + "answers": [ + "response.setStatus(404)", + "response.setErrorCode(404)", + "response.sendError(404)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как отправить ошибку 500 клиенту?", + "answers": [ + "response.setStatus(500)", + "response.sendError(500)", + "throw new ServletException()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для перенаправления клиента на другой URL?", + "answers": [ + "response.sendRedirect("/other")", + "request.forward("/other")", + "response.redirect("/other")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для внутреннего перенаправления (forward) внутри сервера?", + "answers": [ + "response.forward()", + "request.getRequestDispatcher().forward()", + "context.forward()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Где хранится информация о сессии между запросами?", + "answers": [ + "В куках (Cookie) или URL-переписывании", + "В теле HTTP-запроса", + "В заголовке User-Agent" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить объект сессии?", + "answers": [ + "request.getSession()", + "response.getSession()", + "context.getSession()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод уничтожает сессию?", + "answers": [ + "session.invalidate()", + "session.destroy()", + "session.close()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как установить атрибут в сессии?", + "answers": [ + "session.setAttribute("key", value)", + "session.put("key", value)", + "session.add("key", value)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс используется для создания фильтров?", + "answers": [ + "javax.servlet.Filter", + "javax.servlet.http.Filter", + "java.web.Filter" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод фильтра вызывается перед обработкой запроса сервлетом?", + "answers": [ + "doFilter()", + "preProcess()", + "intercept()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как продолжить цепочку фильтров после обработки?", + "answers": [ + "filterChain.next()", + "filterChain.doFilter(request, response)", + "chain.proceed()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой файл используется для конфигурации веб-приложения (до Servlet 3.0)?", + "answers": [ + "web.xml", + "application.xml", + "servlet-config.xml" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как зарегистрировать сервлет с помощью аннотации?", + "answers": [ + "@WebServlet("/hello")", + "@ServletMapping("/hello")", + "@RequestMapping("/hello")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент web.xml определяет маппинг сервлета?", + "answers": [ + "<servlet-mapping>", + "<url-pattern>", + "<route>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой объект предоставляет доступ к параметрам контекста приложения?", + "answers": [ + "ServletContext", + "ServletConfig", + "ApplicationContext" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить параметр контекста из web.xml?", + "answers": [ + "context.getInitParameter("name")", + "context.getAttribute("name")", + "config.getContextParam("name")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод вызывается при уничтожении сервлета?", + "answers": [ + "destroy()", + "finalize()", + "close()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Сколько экземпляров сервлета создаётся по умолчанию на одно приложение?", + "answers": [ + "Один", + "Один на каждый запрос", + "Один на каждую сессию" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Почему сервлеты должны быть потокобезопасными?", + "answers": [ + "Потому что один экземпляр обрабатывает несколько запросов одновременно", + "Потому что они запускаются в отдельных потоках", + "Потому что JVM требует этого" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки Content-Type ответа?", + "answers": [ + "response.setContentType("text/html")", + "response.setHeader("Content-Type", "text/html")", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как установить кодировку ответа в UTF-8?", + "answers": [ + "response.setCharacterEncoding("UTF-8")", + "response.setEncoding("UTF-8")", + "response.setHeader("charset", "UTF-8")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить PrintWriter для записи текстового ответа?", + "answers": [ + "response.getWriter()", + "response.getPrintStream()", + "response.getOutputStream()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить OutputStream для записи бинарного ответа?", + "answers": [ + "response.getOutputStream()", + "response.getBinaryWriter()", + "response.getWriter().asStream()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Можно ли использовать одновременно getWriter() и getOutputStream()?", + "answers": [ + "Да, без ограничений", + "Нет, будет исключение IllegalStateException", + "Только если вызвать flush() между ними" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой интерфейс реализуется для обработки событий жизненного цикла контекста?", + "answers": [ + "ServletContextListener", + "ContextEventListener", + "ApplicationListener" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод вызывается при старте веб-приложения?", + "answers": [ + "contextInitialized()", + "applicationStarted()", + "onStartup()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод вызывается при завершении работы веб-приложения?", + "answers": [ + "contextDestroyed()", + "applicationStopped()", + "onShutdown()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс используется для отслеживания создания/уничтожения сессий?", + "answers": [ + "HttpSessionListener", + "SessionMonitor", + "SessionEventListener" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой заголовок HTTP используется для куков?", + "answers": [ + "Set-Cookie", + "Cookie", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Как создать куку в ответе?", + "answers": [ + "response.addCookie(new Cookie("name", "value"))", + "request.setCookie("name", "value")", + "response.setHeader("Cookie", "name=value")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить все куки из запроса?", + "answers": [ + "request.getCookies()", + "request.getHeaders("Cookie")", + "response.getCookies()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод HttpServletRequest возвращает URI запроса?", + "answers": [ + "getRequestURI()", + "getURL()", + "getPath()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод возвращает полный URL запроса?", + "answers": [ + "request.getRequestURL().toString()", + "request.getFullURL()", + "request.getURL()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, является ли запрос AJAX-запросом?", + "answers": [ + "request.isAjax()", + "Нет стандартного метода; проверяют заголовок X-Requested-With", + "response.isAjaxRequest()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для асинхронной обработки запроса (Servlet 3.0+)?", + "answers": [ + "request.startAsync()", + "response.enableAsync()", + "asyncContext = request.newAsync()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод завершает асинхронную операцию?", + "answers": [ + "asyncContext.complete()", + "response.finishAsync()", + "request.endAsync()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой элемент web.xml включает асинхронную поддержку для сервлета?", + "answers": [ + "<async-supported>true</async-supported>", + "<enable-async/>", + "Асинхронность включена по умолчанию" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки времени жизни сессии (в секундах)?", + "answers": [ + "session.setMaxInactiveInterval(1800)", + "session.setTimeout(1800)", + "context.setSessionTimeout(1800)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод возвращает ID сессии?", + "answers": [ + "session.getId()", + "session.getSessionId()", + "request.getSessionId()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки, новая ли сессия?", + "answers": [ + "session.isNew()", + "session.isCreated()", + "request.isNewSession()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс используется для инициализации сервлета с параметрами?", + "answers": [ + "ServletConfig", + "ServletContext", + "WebInitParam" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить параметр инициализации сервлета?", + "answers": [ + "getServletConfig().getInitParameter("name")", + "getServletContext().getInitParameter("name")", + "request.getInitParam("name")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для отправки JSON-ответа?", + "answers": [ + "response.setContentType("application/json"); writer.print(json)", + "response.sendJSON(json)", + "response.json(json)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой HTTP-статус означает 'Not Modified'?", + "answers": [ + "200", + "304", + "403" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой HTTP-метод идемпотентен?", + "answers": [ + "POST", + "GET", + "PUT" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой HTTP-метод НЕ идемпотентен?", + "answers": [ + "GET", + "PUT", + "POST" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для получения MIME-типа файла?", + "answers": [ + "getServletContext().getMimeType(filename)", + "response.getMimeType(filename)", + "request.detectMimeType(filename)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как прочитать содержимое ресурса из веб-приложения?", + "answers": [ + "getServletContext().getResourceAsStream("/file.txt")", + "new FileInputStream("/file.txt")", + "ClassLoader.getResourceAsStream("file.txt")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод возвращает реальный путь к файлу в веб-приложении?", + "answers": [ + "getServletContext().getRealPath("/file.txt")", + "request.getRealPath("/file.txt")", + "System.getProperty("webapp.root") + "/file.txt"" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс используется для обработки исключений в сервлетах?", + "answers": [ + "Нет специального интерфейса; исключения обрабатываются try-catch", + "ExceptionHandler", + "ServletErrorHandler" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как настроить страницу ошибки в web.xml?", + "answers": [ + "<error-page><error-code>404</error-code><location>/error.html</location></error-page>", + "<exception-handler code="404" page="/error.html" />", + "Нельзя; только программно" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки роли пользователя?", + "answers": [ + "request.isUserInRole("admin")", + "session.hasRole("admin")", + "context.isUserInRole("admin")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как получить имя аутентифицированного пользователя?", + "answers": [ + "request.getRemoteUser()", + "request.getUserPrincipal().getName()", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод проверяет, аутентифицирован ли пользователь?", + "answers": [ + "request.isAuthenticated()", + "request.getRemoteUser() != null", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой элемент web.xml определяет безопасность (security constraint)?", + "answers": [ + "<security-constraint>", + "<auth-config>", + "<login-config>" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой тип аутентификации использует форму входа?", + "answers": [ + "FORM", + "BASIC", + "DIGEST" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для программной аутентификации (Servlet 3.0+)?", + "answers": [ + "request.login(username, password)", + "session.authenticate(username, password)", + "response.authenticateUser(username, password)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как завершить сеанс аутентификации?", + "answers": [ + "request.logout()", + "session.invalidate()", + "Оба метода корректны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой заголовок используется для CORS?", + "answers": [ + "Access-Control-Allow-Origin", + "X-CORS-Origin", + "Origin-Allow" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Как разрешить CORS в сервлете?", + "answers": [ + "response.setHeader("Access-Control-Allow-Origin", "*")", + "request.enableCORS()", + "Невозможно; только на уровне сервера" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод HttpServletRequest возвращает IP-адрес клиента?", + "answers": [ + "getRemoteAddr()", + "getClientIP()", + "getHeader("X-Forwarded-For")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения метода запроса (GET, POST и т.д.)?", + "answers": [ + "request.getMethod()", + "request.getHttpMethod()", + "request.getRequestType()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения заголовка запроса?", + "answers": [ + "request.getHeader("User-Agent")", + "request.getHeaders().get("User-Agent")", + "response.getHeader("User-Agent")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения всех имён заголовков?", + "answers": [ + "request.getHeaderNames()", + "request.getHeaders()", + "request.getAllHeaders()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки заголовка ответа?", + "answers": [ + "response.setHeader("Cache-Control", "no-cache")", + "response.addHeader("Cache-Control", "no-cache")", + "Оба метода, но setHeader заменяет, addHeader — добавляет" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для добавления куки с флагом HttpOnly?", + "answers": [ + "cookie.setHttpOnly(true)", + "response.setHttpOnlyCookie(cookie)", + "Невозможно в Java Servlet API" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки флага Secure у куки?", + "answers": [ + "cookie.setSecure(true)", + "response.setSecureCookie(cookie)", + "cookie.secure = true" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения контекстного пути приложения?", + "answers": [ + "request.getContextPath()", + "getServletContext().getContextPath()", + "request.getServletPath()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод возвращает путь к сервлету?", + "answers": [ + "request.getServletPath()", + "request.getPathInfo()", + "request.getUri()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод возвращает дополнительную часть URL после сервлета?", + "answers": [ + "request.getPathInfo()", + "request.getExtraPath()", + "request.getSubPath()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой интерфейс используется для обработки событий атрибутов сессии?", + "answers": [ + "HttpSessionAttributeListener", + "SessionAttributeListener", + "AttributeEventListener" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод вызывается при добавлении атрибута в сессию?", + "answers": [ + "attributeAdded()", + "onAttributeSet()", + "session.setAttribute() вызывает напрямую" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения диспетчера запросов?", + "answers": [ + "request.getRequestDispatcher("/other")", + "context.getRequestDispatcher("/other")", + "response.createDispatcher("/other")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Можно ли использовать forward после отправки части ответа?", + "answers": [ + "Да, всегда", + "Нет, будет исключение IllegalStateException", + "Только если не вызван getWriter()" + ], + "correctAnswerIndex": 1 + }, + { + "questionText": "Какой метод используется для проверки, был ли отправлен ответ?", + "answers": [ + "response.isCommitted()", + "response.isSent()", + "request.isResponseReady()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для сброса буфера ответа?", + "answers": [ + "response.resetBuffer()", + "response.clear()", + "Нельзя после коммита" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки размера буфера ответа?", + "answers": [ + "response.setBufferSize(8192)", + "response.setBuffer(8192)", + "writer.setBufferSize(8192)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения имени сервера?", + "answers": [ + "request.getServerName()", + "context.getServerName()", + "response.getServerName()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения порта сервера?", + "answers": [ + "request.getServerPort()", + "context.getServerPort()", + "System.getProperty("server.port")" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения протокола запроса?", + "answers": [ + "request.getProtocol()", + "request.getScheme()", + "request.getHttpVersion()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для проверки HTTPS-соединения?", + "answers": [ + "request.isSecure()", + "request.getScheme().equals("https")", + "Оба варианта верны" + ], + "correctAnswerIndex": 2 + }, + { + "questionText": "Какой метод используется для получения локали клиента?", + "answers": [ + "request.getLocale()", + "request.getLanguage()", + "response.getLocale()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для установки локали ответа?", + "answers": [ + "response.setLocale(Locale.ENGLISH)", + "request.setLocale(Locale.ENGLISH)", + "context.setLocale(Locale.ENGLISH)" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения всех параметров запроса как Map?", + "answers": [ + "request.getParameterMap()", + "request.getParameters()", + "request.getAllParameters()" + ], + "correctAnswerIndex": 0 + }, + { + "questionText": "Какой метод используется для получения всех значений параметра (если их несколько)?", + "answers": [ + "request.getParameterValues("name")", + "request.getParameters("name")", + "request.getParameterList("name")" + ], + "correctAnswerIndex": 0 + } +] \ No newline at end of file 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 232fd24..0000000 --- a/src/main/webapp/WEB-INF/edit-user.jsp +++ /dev/null @@ -1,68 +0,0 @@ -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@include file="header.jsp" %> - -
- - -
-
- - - Edit user ${requestScope.user.login} - - -
- -
- - help -
-
- - - - -
- -
- - 3...32 symbols -
-
- - -
- -
- -
-
- - -
- -
- - - - - - -
-
- -
-
-
- -<%@include file="footer.jsp" %> - diff --git a/src/main/webapp/WEB-INF/footer.jsp b/src/main/webapp/WEB-INF/footer.jsp deleted file mode 100644 index 62d09b8..0000000 --- a/src/main/webapp/WEB-INF/footer.jsp +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/header.jsp b/src/main/webapp/WEB-INF/header.jsp deleted file mode 100644 index fac156d..0000000 --- a/src/main/webapp/WEB-INF/header.jsp +++ /dev/null @@ -1,9 +0,0 @@ - - - Edit ${requestScope.user.login} - - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/admin/admin.jsp b/src/main/webapp/WEB-INF/jsp/admin/admin.jsp new file mode 100644 index 0000000..9cad6e0 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/admin/admin.jsp @@ -0,0 +1,115 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+

+ + + + + Панель администратора +

+

+ Управление системой и мониторинг активности пользователей +

+
+ +
+

+ + + + + Основные действия +

+
+ + + + + + + + + + + + +
+ + + +
+ +
+ + + +
+
+
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/admin/statistics.jsp b/src/main/webapp/WEB-INF/jsp/admin/statistics.jsp new file mode 100644 index 0000000..65d7fc6 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/admin/statistics.jsp @@ -0,0 +1,58 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+ +

Статистика тестов

+ +
+

Всего тестов: ${totalTests}

+

Успешно пройдено: ${passedTests}

+
+ +
+

По пользователям

+ + + + + + + + + + + + + + +
ПользовательВсего тестовПройдено
${stat.username}${stat.total}${stat.passed}
+
+ +
+

По темам

+ + + + + + + + + + + + + + + + +
ТемаПопытокУспешно% успеха
${stat.topicDisplayName}${stat.total}${stat.passed}${stat.successRate}
+
+ +
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/admin/users.jsp b/src/main/webapp/WEB-INF/jsp/admin/users.jsp new file mode 100644 index 0000000..ac333d4 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/admin/users.jsp @@ -0,0 +1,72 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+ +

Список пользователей

+ + + + + + + + + + + + + + + + + + + + + + +
ЛогинEmailРольСтатусДействия
${user.username}${user.email}${user.role} + + + Заблокирован + + + Активен + + +
+ +
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/avatar-select.jsp b/src/main/webapp/WEB-INF/jsp/avatar-select.jsp new file mode 100644 index 0000000..d041d3f --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/avatar-select.jsp @@ -0,0 +1,57 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+ +

Аватар профиля

+ +
+

Загрузить свою аватарку

+ +
+ +
+ +
+ + +
+
+ +
+

Выбрать из доступных

+ +
+ +
+ + + +
+ +
+ + Отмена +
+ +
+
+ +
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/common/footer.jsp b/src/main/webapp/WEB-INF/jsp/common/footer.jsp new file mode 100644 index 0000000..d57a12b --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/common/footer.jsp @@ -0,0 +1,8 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + +
+ +
+ diff --git a/src/main/webapp/WEB-INF/jsp/common/header.jsp b/src/main/webapp/WEB-INF/jsp/common/header.jsp new file mode 100644 index 0000000..e0067e7 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/common/header.jsp @@ -0,0 +1,41 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + +
+
+ + +
+ +
+ ${currentUser.nickname} + +
+
+ + + Войти + | + Регистрация + +
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/home.jsp b/src/main/webapp/WEB-INF/jsp/home.jsp new file mode 100644 index 0000000..1a1eb65 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/home.jsp @@ -0,0 +1,93 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+

Добро пожаловать в Java Interview Trainer

+ +
+
+
+

Подготовка к собеседованиям по Java

+

+ Это современное учебное веб-приложение для эффективной подготовки + к техническим собеседованиям. Тренируйтесь в формате, близком к реальному интервью. +

+
+
+
+ + + + + + +
+ Тематические тесты +

Вопросы по ключевым темам Java

+
+
+
+ + + + + +
+ Случайная генерация +

Уникальный набор вопросов каждый раз

+
+
+
+ + + + + + + +
+ Статистика прогресса +

Отслеживайте свой уровень подготовки

+
+
+
+ + + + + + +
+ Реалистичный формат +

Тренировка в условиях интервью

+
+
+
+
+
+ +
+
+ +
+
+
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/layout.jsp b/src/main/webapp/WEB-INF/jsp/layout.jsp new file mode 100644 index 0000000..088b918 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/layout.jsp @@ -0,0 +1,22 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + ${pageTitle != null ? pageTitle : 'Java Interview Trainer'} + + + + + + +
+ +
+ + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp new file mode 100644 index 0000000..ceac49e --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,77 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+
+

Вход в систему

+

Java Interview Trainer

+
+ + +
+ ⚠️ + ${error} +
+
+ +
+
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + + +
+
+ + +
+ + +
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/profile-edit.jsp b/src/main/webapp/WEB-INF/jsp/profile-edit.jsp new file mode 100644 index 0000000..d4d9e42 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile-edit.jsp @@ -0,0 +1,42 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+ +

Редактирование профиля

+ + + +
+ +
+ +
+ +
+ +
+ +
+ + Отмена +
+ +
+ +
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp new file mode 100644 index 0000000..1763ad8 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile.jsp @@ -0,0 +1,187 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+

Профиль пользователя

+
+ + + +
+
+ + +
+

О себе

+

+ + + Информация не указана + + + ${user.about} + + +

+
+
+
+ +
+

+ + + + + История прохождения тестов +

+ + +
+ ℹ️ + Вы еще не проходили тесты. Начните подготовку! +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
ТемаВопросовПравильныхРезультатДата
${result.topicDisplayName}${result.totalQuestions}${result.correctAnswers} + + + + + + + Пройден + + + + + + + + + Не пройден + + + + ${result.formattedFinishedAt}
+
+
+
+ +
+

+ + + + + + Успешность по темам +

+ + +
+ ℹ️ + Пока нет данных для анализа +
+
+ + +
+ +
+
+

${stat.topicDisplayName}

+
+ ${stat.successRate}% +
+
+
+
+ Попыток: + ${stat.total} +
+
+ Успешных: + ${stat.passed} +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/question.jsp b/src/main/webapp/WEB-INF/jsp/question.jsp new file mode 100644 index 0000000..a52e93e --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/question.jsp @@ -0,0 +1,82 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+
+

Вопрос ${questionNumber} из ${totalQuestions}

+
+
+
+
+ ${questionNumber}/${totalQuestions} +
+
+ +
+ + + + + + Темы: + +
+ + ${topic.displayName} + +
+
+
+ +
+
+ Вопрос ${questionNumber} +
+
+

+ ${question.questionText} +

+
+
+ +
+
+ + + +
+ +
+ +
+
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/register.jsp b/src/main/webapp/WEB-INF/jsp/register.jsp new file mode 100644 index 0000000..0aaf45a --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/register.jsp @@ -0,0 +1,92 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+
+

Создание аккаунта

+

Присоединяйтесь к Java Interview Trainer

+
+ + +
+ ⚠️ + ${error} +
+
+ +
+
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + + +
+
+ + +
+ + +
+
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/result.jsp b/src/main/webapp/WEB-INF/jsp/result.jsp new file mode 100644 index 0000000..3656063 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/result.jsp @@ -0,0 +1,154 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+

Результат теста

+
+ +
+
+
+ ${correct} + из ${total} +
+
+

+ + + + + + + Тест пройден! + + + + + + + + Тест не пройден + + +

+

+ Правильных ответов: ${correct} из ${total} (${Math.round(correct * 100 / total)}%) +

+
+ Темы: +
+ + ${topic.trim()} + +
+
+
+
+
+ +
+

+ + + + + + + Рекомендации + + + + + + + Что нужно улучшить + + +

+
+ + +
+ + + + + + Отличный результат! Вы готовы к собеседованию по этим темам. + +
+

+ Поздравляем! Вы успешно прошли тест. Рекомендуем повторить материал через неделю + для закрепления знаний и попробовать более сложные темы. +

+
+ +
+ + + + + + Нужно больше практики по этим темам. + +
+

+ Это учебный проект. Не расстраивайтесь, если тест не пройден с первого раза. + Рекомендуем изучить теорию по слабым темам и попробовать пройти тест снова. +

+
+
+
+
+ + +
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/test-settings.jsp b/src/main/webapp/WEB-INF/jsp/test-settings.jsp new file mode 100644 index 0000000..f2a86fb --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/test-settings.jsp @@ -0,0 +1,98 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + +
+
+

Настройка теста

+

+ Выберите темы и количество вопросов для прохождения теста. + Вопросы подбираются случайным образом из выбранной базы. +

+
+ +
+
+

+ + + + + Темы теста +

+
+ + + +
+
+ +
+

+ + + + + Количество вопросов +

+
+ + + +
+
+ +
+ +
+
+
+ + + diff --git a/src/main/webapp/WEB-INF/list-user.jsp b/src/main/webapp/WEB-INF/list-user.jsp deleted file mode 100644 index ea59fcc..0000000 --- a/src/main/webapp/WEB-INF/list-user.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - List User - - -

User list

- - - id=${user.id}. Edit ${user.login}
-
- - - diff --git a/src/main/webapp/WEB-INF/start-page.jsp b/src/main/webapp/WEB-INF/start-page.jsp deleted file mode 100644 index f273a96..0000000 --- a/src/main/webapp/WEB-INF/start-page.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> - - - - JSP - Hello Page - - -

<%= "Hello Page!" %> -

-
-User list -
- - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 0bf2fcb..0000000 --- a/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/webapp/css/main.css b/src/main/webapp/css/main.css new file mode 100644 index 0000000..357d2e4 --- /dev/null +++ b/src/main/webapp/css/main.css @@ -0,0 +1,1722 @@ +/* базовый сброс */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, sans-serif; + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); + min-height: 100vh; + color: #1f2937; + line-height: 1.5; +} + +/* ============================================= + ОБЩИЙ КОНТЕЙНЕР И ЗАГОЛОВКИ + ============================================= */ + +.container { + max-width: 900px; + margin: 40px auto; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40px; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +h2 { + color: #1a202c; + font-size: 32px; + font-weight: 700; + margin-bottom: 24px; + position: relative; + padding-bottom: 12px; +} + +h2::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 4px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; +} + +h3 { + color: #2d3748; + font-size: 24px; + font-weight: 600; + margin-bottom: 20px; + position: relative; + padding-bottom: 8px; +} + +h3::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 40px; + height: 3px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; +} + +/* ============================================= + HEADER И FOOTER + ============================================= */ + +.app-header { + background: #111827; + color: #ffffff; + padding: 16px 24px; +} + +.app-header a { + color: #ffffff; + text-decoration: none; + margin-right: 12px; +} + +.app-header a:hover { + text-decoration: underline; +} + +.header-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.app-footer { + margin-top: 40px; + padding: 20px 0; + background-color: #f1f5f9; + color: #475569; + font-size: 14px; +} + +.footer-content { + max-width: 900px; + margin: 0 auto; + text-align: center; +} + +/* ============================================= + СОВРЕМЕННЫЕ КНОПКИ + ============================================= */ + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3); + color: white; + text-decoration: none; +} + +.btn-secondary { + background: white; + color: #667eea; + border: 2px solid #667eea; + padding: 10px 22px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; +} + +.btn-secondary:hover { + background: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3); + text-decoration: none; +} + +.btn-large { + padding: 16px 32px; + font-size: 18px; +} + +/* Базовые стили кнопок удалены для избежания конфликтов с .btn-primary и .btn-secondary */ + +.button-icon { + font-size: 18px; + transition: transform 0.2s; +} + +.btn-primary:hover .button-icon { + transform: translateX(4px); +} + +/* ============================================= + КАРТОЧКИ И КОНТЕНТ + ============================================= */ + +.content-card { + background: white; + border-radius: 16px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + border: 1px solid #e2e8f0; + transition: all 0.3s ease; +} + +.content-card:hover { + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + +/* ============================================= + ФОРМЫ И ПОЛЯ ВВОДА + ============================================= */ + +/* Устаревшие стили форм удалены - используются .form-group-modern и .input-wrapper */ + +/* Базовый стиль label все еще нужен для некоторых страниц */ +label { + display: block; + color: #4a5568; + font-weight: 600; + margin-bottom: 8px; + cursor: pointer; +} + +/* ============================================= + СТИЛИ ДЛЯ СТРАНИЦ АВТОРИЗАЦИИ (LOGIN/REGISTER) + ============================================= */ + +.auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.auth-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 40px; + width: 100%; + max-width: 420px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: fadeIn 0.5s ease-out; +} + +.auth-header { + text-align: center; + margin-bottom: 32px; +} + +.auth-header h2 { + color: #1a202c; + font-size: 28px; + font-weight: 700; + margin-bottom: 8px; +} + +.auth-subtitle { + color: #718096; + font-size: 16px; + margin: 0; +} + +.form-group-modern { + margin-bottom: 24px; +} + +.form-group-modern label { + display: block; + color: #4a5568; + font-weight: 600; + margin-bottom: 8px; + font-size: 14px; +} + +.input-wrapper { + position: relative; +} + +.input-wrapper input { + width: 100%; + padding: 12px 16px 12px 44px; + border: 2px solid #e2e8f0; + border-radius: 12px; + font-size: 16px; + transition: all 0.3s ease; + background: white; + box-sizing: border-box; +} + +.input-wrapper input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.input-wrapper input::placeholder { + color: #a0aec0; +} + +.input-icon { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + font-size: 18px; + color: #a0aec0; + pointer-events: none; +} + +.password-toggle { + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #a0aec0; + transition: color 0.2s; +} + +.password-toggle:hover { + color: #4a5568; +} + +.auth-button { + width: 100%; + padding: 14px 24px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 8px; +} + +.auth-button:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); +} + +.auth-button:active { + transform: translateY(0); +} + +.error-alert { + background: #fed7d7; + border: 1px solid #fc8181; + color: #c53030; + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 24px; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.error-icon { + font-size: 16px; +} + +.auth-footer { + text-align: center; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e2e8f0; +} + +.auth-footer p { + color: #718096; + font-size: 14px; + margin: 0; +} + +.auth-footer a { + color: #667eea; + text-decoration: none; + font-weight: 600; + transition: color 0.2s; +} + +.auth-footer a:hover { + color: #5a67d8; + text-decoration: underline; +} + +/* ============================================= + HERO СЕКЦИЯ (HOME PAGE) + ============================================= */ + +.welcome-section { + text-align: center; +} + +.hero-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: center; + margin-bottom: 40px; +} + +.hero-text h3 { + color: #2d3748; + margin-bottom: 16px; +} + +.hero-description { + color: #718096; + font-size: 18px; + line-height: 1.6; + margin-bottom: 0; +} + +.hero-features { + display: grid; + gap: 20px; +} + +.feature-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: #f8fafc; + border-radius: 12px; + transition: all 0.3s ease; +} + +.feature-item:hover { + background: #e2e8f0; + transform: translateX(4px); +} + +.feature-icon { + font-size: 24px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.feature-item strong { + display: block; + color: #2d3748; + margin-bottom: 4px; +} + +.feature-item p { + color: #718096; + margin: 0; + font-size: 14px; +} + +.cta-section { + text-align: center; + margin-top: 40px; +} + +/* ============================================= + ПРОФИЛЬ + ============================================= */ + +.profile-header { + margin-bottom: 32px; +} + +.profile-info { + display: grid; + gap: 24px; +} + +.profile-avatar-section { + display: flex; + align-items: center; + gap: 24px; + padding-bottom: 24px; + border-bottom: 1px solid #e2e8f0; +} + +.profile-avatar-large { + width: 120px; + height: 120px; + border-radius: 20px; + object-fit: cover; + border: 4px solid white; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); +} + +.profile-details h3 { + margin: 0 0 8px 0; + font-size: 28px; +} + +.profile-email { + color: #718096; + margin: 0 0 16px 0; + font-size: 16px; +} + +.profile-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.profile-about h4 { + color: #4a5568; + font-size: 16px; + margin-bottom: 8px; +} + +.profile-about p { + color: #718096; + line-height: 1.6; +} + +.avatar { + margin-top: 8px; + width: 120px; + border-radius: 8px; +} + +.profile-section { + margin-top: 32px; +} + +/* ============================================= + СТАТИСТИКА + ============================================= */ + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +.stat-card { + background: #f8fafc; + border-radius: 12px; + padding: 20px; + border: 1px solid #e2e8f0; +} + +.stat-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.stat-header h4 { + margin: 0; + color: #2d3748; + font-size: 16px; +} + +.stat-percentage { + font-size: 24px; + font-weight: 700; + padding: 4px 8px; + border-radius: 8px; +} + +.stat-percentage.success { + color: #22543d; + background: #c6f6d5; +} + +.stat-percentage.warning { + color: #744210; + background: #faf089; +} + +.stat-percentage.danger { + color: #742a2a; + background: #fed7d7; +} + +.stat-details { + display: flex; + gap: 24px; + margin-bottom: 12px; +} + +.stat-item { + display: flex; + flex-direction: column; +} + +.stat-item span { + color: #718096; + font-size: 12px; + margin-bottom: 4px; +} + +.stat-item strong { + color: #2d3748; + font-size: 16px; +} + +.stat-progress { + height: 8px; + background: #e2e8f0; + border-radius: 4px; + overflow: hidden; +} + +.stat-progress .progress-bar { + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + transition: width 0.3s ease; +} + +/* ============================================= + ВОПРОСЫ И ТЕСТЫ + ============================================= */ + +.test-progress { + margin-bottom: 32px; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.progress-bar-container { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + max-width: 300px; +} + +.progress-bar { + flex: 1; + height: 8px; + background: #e2e8f0; + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-text { + font-weight: 600; + color: #667eea; + font-size: 14px; + min-width: 50px; +} + +.test-topics { + margin-bottom: 24px; + font-size: 14px; + color: #475569; +} + +.test-topics ul { + margin-top: 8px; + padding-left: 18px; +} + +.topic-tags { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 8px; +} + +.topic-tag { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; +} + +.question-header { + margin-bottom: 16px; +} + +.question-card { + margin-bottom: 32px; +} + +.question-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.question-number { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 600; +} + +.question-text { + font-size: 18px; + line-height: 1.6; + color: #2d3748; + margin: 0; +} + +.answer-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.answers-container { + display: grid; + gap: 12px; + margin-bottom: 32px; +} + +.answer-option-modern { + display: flex; + align-items: center; + padding: 20px; + border: 2px solid #e2e8f0; + border-radius: 16px; + cursor: pointer; + transition: all 0.3s ease; + background: white; + position: relative; +} + +.answer-option-modern:hover { + border-color: #667eea; + background: #f8fafc; + transform: translateX(4px); +} + +.answer-option-modern input[type="radio"] { + display: none; +} + +.answer-option-modern input[type="radio"]:checked + .answer-content { + color: #667eea; +} + +.answer-option-modern input[type="radio"]:checked ~ .answer-indicator { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; +} + +.answer-option-modern input[type="radio"]:checked ~ .answer-indicator::after { + opacity: 1; +} + +.answer-content { + display: flex; + align-items: center; + gap: 16px; + flex: 1; +} + +.answer-checkbox { + width: 40px; + height: 40px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.checkmark { + width: 20px; + height: 20px; +} + +.checkmark-path { + stroke: #667eea; + stroke-width: 3; + fill: none; + stroke-dasharray: 100; + stroke-dashoffset: 100; + transition: stroke-dashoffset 0.3s ease; +} + +.answer-option-modern input[type="radio"]:checked ~ .answer-content .answer-checkbox { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; +} + +.answer-option-modern input[type="radio"]:checked ~ .answer-content .checkmark-path { + stroke: white; + stroke-dashoffset: 0; +} + +.answer-option-modern:hover .answer-checkbox { + border-color: #667eea; + transform: scale(1.05); +} + +.answer-option-modern:hover .checkmark-path { + stroke-dashoffset: 50; +} + +.answer-text { + flex: 1; + color: #4a5568; + font-size: 16px; +} + +.answer-indicator { + width: 24px; + height: 24px; + border: 2px solid #e2e8f0; + border-radius: 50%; + position: relative; + transition: all 0.3s ease; +} + +.answer-indicator::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + background: white; + border-radius: 50%; + opacity: 0; + transition: opacity 0.3s ease; +} + +.question-actions { + text-align: center; +} + +/* ============================================= + ТАБЛИЦЫ И СТАТУСЫ + ============================================= */ + +.modern-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.modern-table th { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 16px; + text-align: left; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.modern-table td { + padding: 16px; + border-bottom: 1px solid #e2e8f0; + color: #4a5568; +} + +.modern-table tr:last-child td { + border-bottom: none; +} + +.modern-table tr:hover { + background: #f8fafc; +} + +.data-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.data-table th, +.data-table td { + padding: 8px; + border-bottom: 1px solid #e5e7eb; + text-align: left; +} + +.data-table th { + background-color: #f1f5f9; +} + +.status-badge { + display: inline-flex; + align-items: center; + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-badge.success { + background: #c6f6d5; + color: #22543d; +} + +.status-badge.fail { + background: #fed7d7; + color: #742a2a; +} + +.status-badge.pending { + background: #e2e8f0; + color: #4a5568; +} + +.status-success { + color: #15803d; + font-weight: 600; +} + +.status-fail { + color: #b91c1c; + font-weight: 600; +} + +.result-success { + color: #15803d; + font-weight: 600; + margin-top: 16px; +} + +.result-fail { + color: #b91c1c; + font-weight: 600; + margin-top: 16px; +} + +.result-hint { + margin-top: 8px; + color: #475569; +} + +/* ============================================= + АЛЕРТЫ И УВЕДОМЛЕНИЯ + ============================================= */ + +.modern-alert { + padding: 16px 20px; + border-radius: 12px; + margin-bottom: 24px; + display: flex; + align-items: center; + gap: 12px; + font-weight: 500; +} + +.modern-alert.success { + background: #c6f6d5; + border: 1px solid #9ae6b4; + color: #22543d; +} + +.modern-alert.error { + background: #fed7d7; + border: 1px solid #fc8181; + color: #742a2a; +} + +.modern-alert.info { + background: #bee3f8; + border: 1px solid #90cdf4; + color: #2c5282; +} + +.form-error { + margin-bottom: 16px; + color: #b91c1c; + font-weight: 500; +} + +/* ============================================= + АВАТАРЫ + ============================================= */ + +.avatar-section { + margin-top: 32px; +} + +.avatar-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 16px; + margin-top: 16px; +} + +.avatar-option { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; +} + +.avatar-option input { + margin-bottom: 8px; +} + +.avatar-option img { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 8px; + border: 2px solid transparent; +} + +.avatar-option input:checked + img { + border-color: #2563eb; +} + +.avatar-actions { + margin-top: 24px; + display: flex; + gap: 16px; +} + +.avatar-actions a { + align-self: center; + color: #475569; + text-decoration: none; +} + +.avatar-actions a:hover { + text-decoration: underline; +} + +/* ============================================= + АДМИН ПАНЕЛЬ + ============================================= */ + +.admin-note { + margin-bottom: 16px; + color: #475569; +} + +.admin-menu { + list-style: none; + padding: 0; + margin-top: 16px; +} + +.admin-menu li { + margin-bottom: 12px; +} + +.admin-menu a { + color: #2563eb; + text-decoration: none; + font-weight: 500; +} + +.admin-menu a:hover { + text-decoration: underline; +} + +.admin-disabled { + color: #94a3b8; +} + +.stats-summary { + margin-bottom: 24px; +} + +.stats-summary p { + margin-bottom: 4px; +} + +.stats-section { + margin-top: 32px; +} + +.user-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.user-actions form { + display: inline-flex; + gap: 6px; + align-items: center; +} + +.user-actions select { + padding: 4px 6px; + border-radius: 6px; + border: 1px solid #cbd5f5; +} + +/* ============================================= + ОБЩИЕ ЭЛЕМЕНТЫ + ============================================= */ + +.actions { + margin-top: 24px; + display: flex; + gap: 16px; +} + +.actions a { + text-decoration: none; + color: #2563eb; + font-weight: 500; +} + +.actions a:hover { + text-decoration: underline; +} + +.form-actions { + margin-top: 16px; + display: flex; + gap: 16px; + align-items: center; +} + +.form-actions a { + color: #475569; + text-decoration: none; +} + +.form-actions a:hover { + text-decoration: underline; +} + +.container ul { + margin: 16px 0; + padding-left: 20px; +} + +.container li { + margin-bottom: 8px; +} + +/* ============================================= + АНИМАЦИИ + ============================================= */ + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ============================================= + АДАПТИВНОСТЬ + ============================================= */ + +@media (max-width: 768px) { + .container { + margin: 20px; + padding: 24px; + } + + h2 { + font-size: 28px; + } + + h3 { + font-size: 20px; + } + + .hero-content { + grid-template-columns: 1fr; + gap: 24px; + } + + .profile-avatar-section { + flex-direction: column; + text-align: center; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .progress-header { + flex-direction: column; + gap: 16px; + align-items: flex-start; + } + + .progress-bar-container { + max-width: 100%; + } + + .answer-content { + gap: 12px; + } + + .answer-checkbox { + width: 32px; + height: 32px; + } + + .checkmark { + width: 16px; + height: 16px; + } + + .answer-text { + font-size: 14px; + } + + .modern-table { + font-size: 14px; + } + + .modern-table th, + .modern-table td { + padding: 12px 8px; + } +} + +@media (max-width: 480px) { + .auth-container { + padding: 16px; + } + + .auth-card { + padding: 32px 24px; + } + + .auth-header h2 { + font-size: 24px; + } + + .input-wrapper input { + padding: 14px 16px 14px 44px; + font-size: 16px; + } +} + +/* ============================================= + СТИЛИ ДЛЯ TEST SETTINGS + ============================================= */ + +.test-settings-header { + text-align: center; + margin-bottom: 40px; +} + +.test-settings-description { + color: #718096; + font-size: 16px; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +.test-settings-form { + display: grid; + gap: 32px; +} + +.topics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; +} + +.topic-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; +} + +.topic-card:hover { + border-color: #667eea; + background: white; + transform: translateY(-2px); +} + +.topic-card input[type="checkbox"] { + display: none; +} + +.topic-card input[type="checkbox"]:checked + .topic-content { + color: #667eea; +} + +.topic-card input[type="checkbox"]:checked ~ .topic-check { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; +} + +.topic-card input[type="checkbox"]:checked ~ .topic-check svg { + stroke: white; + opacity: 1; +} + +.topic-content { + display: flex; + align-items: center; + gap: 12px; +} + +.topic-icon { + font-size: 20px; +} + +.topic-name { + font-weight: 600; + color: #4a5568; +} + +.topic-check { + width: 24px; + height: 24px; + border: 2px solid #e2e8f0; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.topic-check svg { + stroke: #667eea; + opacity: 0; + transition: opacity 0.2s ease; +} + +.question-count-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} + +.count-option { + display: block; + cursor: pointer; +} + +.count-option input[type="radio"] { + display: none; +} + +.count-option input[type="radio"]:checked + .count-content { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: #667eea; +} + +.count-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + transition: all 0.3s ease; +} + +.count-content:hover { + border-color: #667eea; + background: white; +} + +.count-number { + font-size: 32px; + font-weight: 700; + margin-bottom: 8px; +} + +.count-label { + font-size: 14px; + font-weight: 500; +} + +.test-settings-actions { + text-align: center; + margin-top: 32px; +} + +/* ============================================= + СТИЛИ ДЛЯ RESULT PAGE + ============================================= */ + +.result-header { + text-align: center; + margin-bottom: 32px; +} + +.result-stats { + text-align: center; +} + +.result-score { + display: flex; + align-items: center; + gap: 40px; + margin-bottom: 32px; +} + +.score-circle { + width: 120px; + height: 120px; + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-weight: 700; + position: relative; +} + +.score-circle.success { + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); + color: white; +} + +.score-circle.fail { + background: linear-gradient(135deg, #fc8181 0%, #f56565 100%); + color: white; +} + +.score-number { + font-size: 36px; + line-height: 1; +} + +.score-total { + font-size: 14px; + opacity: 0.8; +} + +.score-details h3 { + margin: 0 0 16px 0; + font-size: 28px; +} + +.result-description { + color: #718096; + margin: 0 0 24px 0; + font-size: 16px; +} + +.result-topics { + text-align: left; +} + +.result-topics strong { + color: #4a5568; +} + +.result-recommendations h3 { + margin-bottom: 20px; +} + +.recommendations-content { + display: grid; + gap: 20px; +} + +.recommendation-text { + color: #718096; + line-height: 1.6; + margin: 0; +} + +.result-actions { + display: flex; + gap: 16px; + justify-content: center; + margin-top: 40px; + flex-wrap: wrap; +} + +/* ============================================= + СТИЛИ ДЛЯ ADMIN PAGE + ============================================= */ + +.admin-header { + text-align: center; + margin-bottom: 32px; +} + +.admin-description { + color: #718096; + font-size: 16px; + margin: 8px 0 0 0; +} + +.admin-alert { + background: #e6fffa; + border: 1px solid #81e6d9; + color: #234e52; + padding: 16px 20px; + border-radius: 12px; + margin-bottom: 32px; + display: flex; + align-items: center; + gap: 12px; + font-weight: 500; +} + +.admin-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 20px; + margin-bottom: 32px; +} + +.admin-stat-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: white; + border: 2px solid #e2e8f0; + border-radius: 12px; + transition: all 0.3s ease; + cursor: pointer; +} + +.admin-stat-card:hover:not(.disabled) { + border-color: #667eea; + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(102, 126, 234, 0.1); +} + +.admin-stat-card.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.stat-icon { + font-size: 32px; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + background: #f8fafc; + border-radius: 12px; +} + +.stat-info h4 { + margin: 0 0 4px 0; + color: #2d3748; + font-size: 18px; +} + +.stat-info p { + margin: 0; + color: #718096; + font-size: 14px; +} + +.admin-menu-card h3 { + margin-bottom: 24px; +} + +.admin-menu-grid { + display: grid; + gap: 16px; +} + +.admin-menu-item { + display: flex; + align-items: center; + gap: 20px; + padding: 20px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + text-decoration: none; + color: inherit; + transition: all 0.3s ease; +} + +.admin-menu-item:hover:not(.disabled) { + border-color: #667eea; + background: white; + transform: translateX(4px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); +} + +.admin-menu-item.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.menu-icon { + font-size: 24px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: white; + border-radius: 10px; +} + +.menu-content { + flex: 1; +} + +.menu-content h4 { + margin: 0 0 4px 0; + color: #2d3748; + font-size: 16px; +} + +.menu-content p { + margin: 0; + color: #718096; + font-size: 14px; +} + +.menu-arrow { + font-size: 20px; + color: #a0aec0; + transition: all 0.3s ease; +} + +.admin-menu-item:hover:not(.disabled) .menu-arrow { + color: #667eea; + transform: translateX(4px); +} + +/* ============================================= + АДАПТИВНОСТЬ ДЛЯ НОВЫХ СТРАНИЦ + ============================================= */ + +@media (max-width: 768px) { + .result-score { + flex-direction: column; + gap: 24px; + } + + .score-circle { + width: 100px; + height: 100px; + } + + .score-number { + font-size: 28px; + } + + .admin-stats-grid { + grid-template-columns: 1fr; + } + + .result-actions { + flex-direction: column; + align-items: center; + } + + .result-actions .btn-primary, + .result-actions .btn-secondary { + width: 100%; + max-width: 300px; + justify-content: center; + } +} + +/* Стили для подсказок форм */ +.form-hint { + display: block; + margin-top: 6px; + font-size: 12px; + color: #718096; + font-style: italic; + opacity: 0.8; + transition: opacity 0.3s ease; +} + +.form-group-modern:hover .form-hint { + opacity: 1; +} + +/* Улучшение для title атрибута */ +input[title]:hover { + cursor: help; +} + + diff --git a/src/main/webapp/images/img.png b/src/main/webapp/images/img.png deleted file mode 100644 index 1000db3..0000000 Binary files a/src/main/webapp/images/img.png and /dev/null differ diff --git a/src/main/webapp/js/auth-scripts.js b/src/main/webapp/js/auth-scripts.js new file mode 100644 index 0000000..b31e0fb --- /dev/null +++ b/src/main/webapp/js/auth-scripts.js @@ -0,0 +1,51 @@ +document.addEventListener('DOMContentLoaded', function() { + const passwordToggles = document.querySelectorAll('.password-toggle'); + + passwordToggles.forEach(button => { + button.addEventListener('click', function() { + const input = this.previousElementSibling; + const type = input.type === 'password' ? 'text' : 'password'; + input.type = type; + this.textContent = type === 'password' ? '👁️' : '👁️‍🗨️'; + }); + }); + + const inputs = document.querySelectorAll('input[required]'); + + inputs.forEach(input => { + input.addEventListener('blur', function() { + validateField(this); + }); + + input.addEventListener('input', function() { + if (this.classList.contains('error')) { + validateField(this); + } + }); + }); + + function validateField(field) { + if (field.validity.valid && field.value.trim() !== '') { + field.style.borderColor = '#48bb78'; + field.classList.remove('error'); + } else if (field.value.trim() !== '') { + field.style.borderColor = '#fc8181'; + field.classList.add('error'); + } else { + field.style.borderColor = '#e2e8f0'; + field.classList.remove('error'); + } + } + + const authCard = document.querySelector('.auth-card'); + if (authCard) { + authCard.style.opacity = '0'; + authCard.style.transform = 'translateY(20px)'; + + setTimeout(() => { + authCard.style.transition = 'all 0.5s ease-out'; + authCard.style.opacity = '1'; + authCard.style.transform = 'translateY(0)'; + }, 100); + } +}); \ No newline at end of file diff --git a/src/main/webapp/resources/avatars/default/avatar1.png b/src/main/webapp/resources/avatars/default/avatar1.png new file mode 100644 index 0000000..7a1c64f Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar1.png differ diff --git a/src/main/webapp/resources/avatars/default/avatar2.png b/src/main/webapp/resources/avatars/default/avatar2.png new file mode 100644 index 0000000..113ce9d Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar2.png differ diff --git a/src/main/webapp/resources/avatars/default/avatar3.png b/src/main/webapp/resources/avatars/default/avatar3.png new file mode 100644 index 0000000..9f23dbb Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar3.png differ diff --git a/src/main/webapp/resources/avatars/default/avatar4.png b/src/main/webapp/resources/avatars/default/avatar4.png new file mode 100644 index 0000000..98baa08 Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar4.png differ diff --git a/src/main/webapp/resources/avatars/default/avatar5.png b/src/main/webapp/resources/avatars/default/avatar5.png new file mode 100644 index 0000000..73b5a08 Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar5.png differ diff --git a/src/main/webapp/resources/avatars/default/avatar6.png b/src/main/webapp/resources/avatars/default/avatar6.png new file mode 100644 index 0000000..924e192 Binary files /dev/null and b/src/main/webapp/resources/avatars/default/avatar6.png differ diff --git a/src/main/webapp/resources/avatars/default/default.png b/src/main/webapp/resources/avatars/default/default.png new file mode 100644 index 0000000..3402a60 Binary files /dev/null and b/src/main/webapp/resources/avatars/default/default.png differ diff --git a/src/test/java/com/javarush/zyibin/exception/AuthenticationExceptionTest.java b/src/test/java/com/javarush/zyibin/exception/AuthenticationExceptionTest.java new file mode 100644 index 0000000..892426b --- /dev/null +++ b/src/test/java/com/javarush/zyibin/exception/AuthenticationExceptionTest.java @@ -0,0 +1,156 @@ +package com.javarush.zyibin.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AuthenticationExceptionTest { + + @Test + void shouldCreateAuthenticationException_withMessageAndReason() { + String message = "Authentication failed"; + String reason = "INVALID_CREDENTIALS"; + + AuthenticationException exception = new AuthenticationException(message, reason); + + assertEquals(message, exception.getMessage()); + assertEquals(reason, exception.getReason()); + } + + @Test + void shouldCreateInvalidCredentialsException() { + AuthenticationException exception = AuthenticationException.invalidCredentials(); + + assertEquals("Invalid login or password", exception.getMessage()); + assertEquals("INVALID_CREDENTIALS", exception.getReason()); + } + + @Test + void shouldCreateUserBlockedException() { + AuthenticationException exception = AuthenticationException.userBlocked(); + + assertEquals("User is blocked", exception.getMessage()); + assertEquals("USER_BLOCKED", exception.getReason()); + } + + @Test + void shouldCreateUserNotFoundException() { + AuthenticationException exception = AuthenticationException.userNotFound(); + + assertEquals("User not found", exception.getMessage()); + assertEquals("USER_NOT_FOUND", exception.getReason()); + } + + @Test + void shouldInheritFromRuntimeException() { + AuthenticationException exception = new AuthenticationException("Test message", "TEST_REASON"); + + assertTrue(exception instanceof RuntimeException); + } + + @Test + void shouldHandleNullMessage() { + String reason = "TEST_REASON"; + + AuthenticationException exception = new AuthenticationException(null, reason); + + assertNull(exception.getMessage()); + assertEquals(reason, exception.getReason()); + } + + @Test + void shouldHandleNullReason() { + String message = "Test message"; + + AuthenticationException exception = new AuthenticationException(message, null); + + assertEquals(message, exception.getMessage()); + assertNull(exception.getReason()); + } + + @Test + void shouldHandleEmptyMessage() { + String message = ""; + String reason = "TEST_REASON"; + + AuthenticationException exception = new AuthenticationException(message, reason); + + assertEquals(message, exception.getMessage()); + assertEquals(reason, exception.getReason()); + } + + @Test + void shouldHandleEmptyReason() { + String message = "Test message"; + String reason = ""; + + AuthenticationException exception = new AuthenticationException(message, reason); + + assertEquals(message, exception.getMessage()); + assertEquals(reason, exception.getReason()); + } + + @Test + void shouldCreateDifferentExceptionTypes() { + AuthenticationException invalidCredentialsException = AuthenticationException.invalidCredentials(); + AuthenticationException userBlockedException = AuthenticationException.userBlocked(); + AuthenticationException userNotFoundException = AuthenticationException.userNotFound(); + + assertEquals("INVALID_CREDENTIALS", invalidCredentialsException.getReason()); + assertEquals("USER_BLOCKED", userBlockedException.getReason()); + assertEquals("USER_NOT_FOUND", userNotFoundException.getReason()); + + assertEquals("Invalid login or password", invalidCredentialsException.getMessage()); + assertEquals("User is blocked", userBlockedException.getMessage()); + assertEquals("User not found", userNotFoundException.getMessage()); + } + + @Test + void shouldBeCatchableAsRuntimeException() { + AuthenticationException authException = AuthenticationException.invalidCredentials(); + + RuntimeException caughtException = assertThrows(RuntimeException.class, () -> { + throw authException; + }); + + assertTrue(caughtException instanceof AuthenticationException); + assertEquals("Invalid login or password", caughtException.getMessage()); + assertEquals("INVALID_CREDENTIALS", ((AuthenticationException) caughtException).getReason()); + } + + @Test + void shouldMaintainImmutabilityOfStaticMethods() { + AuthenticationException exception1 = AuthenticationException.invalidCredentials(); + AuthenticationException exception2 = AuthenticationException.invalidCredentials(); + + assertEquals(exception1.getMessage(), exception2.getMessage()); + assertEquals(exception1.getReason(), exception2.getReason()); + assertNotSame(exception1, exception2); + } + + @Test + void shouldHandleCustomMessageAndReason() { + String customMessage = "Custom authentication error"; + String customReason = "CUSTOM_ERROR"; + + AuthenticationException exception = new AuthenticationException(customMessage, customReason); + + assertEquals(customMessage, exception.getMessage()); + assertEquals(customReason, exception.getReason()); + } + + @Test + void shouldWorkInTryCatchBlock() { + AuthenticationException caughtException = null; + + try { + throw AuthenticationException.userBlocked(); + } catch (AuthenticationException e) { + caughtException = e; + } + + assertNotNull(caughtException); + assertEquals("User is blocked", caughtException.getMessage()); + assertEquals("USER_BLOCKED", caughtException.getReason()); + } +} diff --git a/src/test/java/com/javarush/zyibin/exception/ValidationExceptionTest.java b/src/test/java/com/javarush/zyibin/exception/ValidationExceptionTest.java new file mode 100644 index 0000000..c2c93e1 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/exception/ValidationExceptionTest.java @@ -0,0 +1,172 @@ +package com.javarush.zyibin.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ValidationExceptionTest { + + @Test + void shouldCreateValidationException_withMessageAndField() { + String message = "Invalid input"; + String field = "username"; + + ValidationException exception = new ValidationException(message, field); + + assertEquals(message, exception.getMessage()); + assertEquals(field, exception.getField()); + assertEquals("VALIDATION_ERROR", exception.getErrorCode()); + } + + @Test + void shouldCreateValidationException_withMessageFieldAndErrorCode() { + String message = "Invalid email format"; + String field = "email"; + String errorCode = "EMAIL_FORMAT_INVALID"; + + ValidationException exception = new ValidationException(message, field, errorCode); + + assertEquals(message, exception.getMessage()); + assertEquals(field, exception.getField()); + assertEquals(errorCode, exception.getErrorCode()); + } + + @Test + void shouldCreateUsernameValidationException() { + String message = "Username is too short"; + + ValidationException exception = ValidationException.username(message); + + assertEquals(message, exception.getMessage()); + assertEquals("username", exception.getField()); + assertEquals("USERNAME_INVALID", exception.getErrorCode()); + } + + @Test + void shouldCreatePasswordValidationException() { + String message = "Password must contain numbers"; + + ValidationException exception = ValidationException.password(message); + + assertEquals(message, exception.getMessage()); + assertEquals("password", exception.getField()); + assertEquals("PASSWORD_INVALID", exception.getErrorCode()); + } + + @Test + void shouldCreateEmailValidationException() { + String message = "Email format is invalid"; + + ValidationException exception = ValidationException.email(message); + + assertEquals(message, exception.getMessage()); + assertEquals("email", exception.getField()); + assertEquals("EMAIL_INVALID", exception.getErrorCode()); + } + + @Test + void shouldCreateGeneralValidationException() { + String message = "General validation error"; + + ValidationException exception = ValidationException.general(message); + + assertEquals(message, exception.getMessage()); + assertEquals("general", exception.getField()); + assertEquals("GENERAL_ERROR", exception.getErrorCode()); + } + + @Test + void shouldInheritFromRuntimeException() { + ValidationException exception = new ValidationException("Test message", "test"); + + assertTrue(exception instanceof RuntimeException); + } + + @Test + void shouldHandleNullMessage() { + String field = "test"; + + ValidationException exception = new ValidationException(null, field); + + assertNull(exception.getMessage()); + assertEquals(field, exception.getField()); + assertEquals("VALIDATION_ERROR", exception.getErrorCode()); + } + + @Test + void shouldHandleNullField() { + String message = "Test message"; + + ValidationException exception = new ValidationException(message, null); + + assertEquals(message, exception.getMessage()); + assertNull(exception.getField()); + assertEquals("VALIDATION_ERROR", exception.getErrorCode()); + } + + @Test + void shouldHandleEmptyMessage() { + String message = ""; + String field = "test"; + + ValidationException exception = new ValidationException(message, field); + + assertEquals(message, exception.getMessage()); + assertEquals(field, exception.getField()); + assertEquals("VALIDATION_ERROR", exception.getErrorCode()); + } + + @Test + void shouldHandleEmptyField() { + String message = "Test message"; + String field = ""; + + ValidationException exception = new ValidationException(message, field); + + assertEquals(message, exception.getMessage()); + assertEquals(field, exception.getField()); + assertEquals("VALIDATION_ERROR", exception.getErrorCode()); + } + + @Test + void shouldHandleNullErrorCode() { + String message = "Test message"; + String field = "test"; + + ValidationException exception = new ValidationException(message, field, null); + + assertEquals(message, exception.getMessage()); + assertEquals(field, exception.getField()); + assertNull(exception.getErrorCode()); + } + + @Test + void shouldCreateDifferentExceptionTypes() { + ValidationException usernameException = ValidationException.username("Username error"); + ValidationException passwordException = ValidationException.password("Password error"); + ValidationException emailException = ValidationException.email("Email error"); + ValidationException generalException = ValidationException.general("General error"); + + assertEquals("USERNAME_INVALID", usernameException.getErrorCode()); + assertEquals("PASSWORD_INVALID", passwordException.getErrorCode()); + assertEquals("EMAIL_INVALID", emailException.getErrorCode()); + assertEquals("GENERAL_ERROR", generalException.getErrorCode()); + + assertEquals("username", usernameException.getField()); + assertEquals("password", passwordException.getField()); + assertEquals("email", emailException.getField()); + assertEquals("general", generalException.getField()); + } + + @Test + void shouldBeCatchableAsRuntimeException() { + ValidationException validationException = ValidationException.username("Test error"); + + RuntimeException caughtException = assertThrows(RuntimeException.class, () -> { + throw validationException; + }); + + assertTrue(caughtException instanceof ValidationException); + assertEquals("Test error", caughtException.getMessage()); + } +} diff --git a/src/test/java/com/javarush/zyibin/model/InterviewStateTest.java b/src/test/java/com/javarush/zyibin/model/InterviewStateTest.java new file mode 100644 index 0000000..cd0a06b --- /dev/null +++ b/src/test/java/com/javarush/zyibin/model/InterviewStateTest.java @@ -0,0 +1,86 @@ +package com.javarush.zyibin.model; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + + class InterviewStateTest { + + private InterviewState interviewState; + private Question firstQuestion; + private Question secondQuestion; + + @BeforeEach + void setUp() { + Set topics = Set.of(Topic.JAVA_CORE); + firstQuestion = new Question( + "Что такое JVM?", + List.of("Java Virtual Machine", "JavaScript VM"), + 0 + ); + + secondQuestion = new Question( + "Что такое JDK?", + List.of("Java Development Kit", "Java Debug Kit"), + 0 + ); + + List questions = List.of(firstQuestion, secondQuestion); + interviewState = new InterviewState(topics, questions); + } + + @Test + void shouldReturnFirstQuestion_whenTestJustStarted() { + + Question currentQuestion = interviewState.getCurrentQuestion(); + + assertEquals(0, interviewState.getCurrentIndex()); + assertEquals(firstQuestion, currentQuestion); + } + + @Test + void shouldMoveToNextQuestion_whenMoveToNextQuestionCalled() { + + interviewState.moveToNextQuestion(); + + assertEquals(1, interviewState.getCurrentIndex()); + assertSame(secondQuestion, interviewState.getCurrentQuestion()); + } + + @Test + void shouldIncreaseScore_whenIncrementScoreCalled() { + + interviewState.incrementScore(); + interviewState.incrementScore(); + + assertEquals(2, interviewState.getScore()); + } + + @Test + void shouldNotBeFinished_whenQuestionsRemain() { + + boolean finished = interviewState.isFinished(); + + assertFalse(finished); + } + + @Test + void shouldBeFinished_whenAllQuestionsAnswered() { + + interviewState.moveToNextQuestion(); + interviewState.moveToNextQuestion(); + + assertTrue(interviewState.isFinished()); + } + + @Test + void shouldReturnCorrectTotalQuestionsCount() { + assertEquals(2, interviewState.getTotalQuestions()); + } +} diff --git a/src/test/java/com/javarush/zyibin/model/QuestionTest.java b/src/test/java/com/javarush/zyibin/model/QuestionTest.java new file mode 100644 index 0000000..19bf607 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/model/QuestionTest.java @@ -0,0 +1,47 @@ +package com.javarush.zyibin.model; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuestionTest { + + @Test + void shouldCreateQuestionUsingConstructor() { + String text = "Что такое JVM?"; + List answer = List.of("VM", "JDK", "JRE"); + int correctIndex = 0; + + Question question = new Question(text, answer, correctIndex); + + assertEquals(text, question.getQuestionText()); + assertEquals(answer, question.getAnswers()); + assertEquals(correctIndex, question.getCorrectAnswerIndex()); + } + + @SneakyThrows + @Test + void shouldDeserializeQuestionFromJson() { + + String json = """ + { + "questionText": "Что такое JVM?", + "answers": ["VM", "JDK", "JRE"], + "correctAnswerIndex": 0 + } + """; + + ObjectMapper mapper= new ObjectMapper(); + Question question = mapper.readValue(json, Question.class); + + assertEquals("Что такое JVM?", question.getQuestionText()); + assertEquals(List.of("VM", "JDK", "JRE"), question.getAnswers()); + assertEquals(0, question.getCorrectAnswerIndex()); + } + +} diff --git a/src/test/java/com/javarush/zyibin/model/TestResultTest.java b/src/test/java/com/javarush/zyibin/model/TestResultTest.java new file mode 100644 index 0000000..5d12b74 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/model/TestResultTest.java @@ -0,0 +1,51 @@ +package com.javarush.zyibin.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestResultTest { + + private TestResult result; + + @BeforeEach + void setUp() { + result = new TestResult( + 1L, + "java-core", + 10, + 7, + true, + LocalDateTime.now() + ); + } + + @Test + void shouldCreateTestResultWithGivenValues() { + + assertEquals(1L, result.getUserId()); + assertEquals("java-core", result.getTopicCode()); + assertEquals(10, result.getTotalQuestions()); + assertEquals(7, result.getCorrectAnswers()); + assertTrue(result.isPassed()); + assertNotNull(result.getFinishedAt()); + } + + @Test + void shouldAllowSettingIdOnceWhenIdIsZero() { + + result.setId(100L); + + assertEquals(100L, result.getId()); + } + + @Test + void shouldThrowExceptionWhenSettingIdSecondTime() { + result.setId(100L); + + assertThrows(IllegalStateException.class, () -> result.setId(200L)); + } +} diff --git a/src/test/java/com/javarush/zyibin/model/TopicTest.java b/src/test/java/com/javarush/zyibin/model/TopicTest.java new file mode 100644 index 0000000..7d10c97 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/model/TopicTest.java @@ -0,0 +1,37 @@ +package com.javarush.zyibin.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TopicTest { + + @Test + void shouldReturnCorrectTopic_whenValidCodeProvided() { + + Topic topic = Topic.fromCode("java-core"); + + assertEquals(Topic.JAVA_CORE, topic); + assertEquals("Java Core", topic.getDisplayName()); + } + + @Test + void shouldThrowException_whenUnknownCodeProvided() { + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, + () -> Topic.fromCode("unknown-topic")); + + assertTrue(exception.getMessage().contains("Unknown topic code")); + } + + @Test + void shouldReturnCorrectCodeAndDisplayName() { + + Topic topic = Topic.JUNIT; + + assertEquals("junit5", topic.getCode()); + assertEquals("JUnit 5", topic.getDisplayName()); + } + +} diff --git a/src/test/java/com/javarush/zyibin/model/UserTest.java b/src/test/java/com/javarush/zyibin/model/UserTest.java new file mode 100644 index 0000000..a94d19d --- /dev/null +++ b/src/test/java/com/javarush/zyibin/model/UserTest.java @@ -0,0 +1,122 @@ +package com.javarush.zyibin.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserTest { + + @Test + void shouldCreateUserWithDefaultValues() { + + User user = new User( + 1L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + assertEquals(1L, user.getId()); + assertEquals("john", user.getUsername()); + assertEquals("hash", user.getPasswordHash()); + assertEquals("john@mail.com", user.getEmail()); + assertEquals(Role.USER, user.getRole()); + + assertEquals("john", user.getNickname()); + assertEquals("", user.getAbout()); + assertEquals("/avatars/default/default.png", user.getAvatarPath()); + + assertFalse(user.isBlocked()); + assertNotNull(user.getCreatedAt()); + } + + @Test + void shouldAllowChangingProfileFields() { + + User user = new User( + 1L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + user.setNickname("Johnny"); + user.setAbout("Java developer"); + user.setAvatarPath("/avatars/custom/john.png"); + + assertEquals("Johnny", user.getNickname()); + assertEquals("Java developer", user.getAbout()); + assertEquals("/avatars/custom/john.png", user.getAvatarPath()); + } + + @Test + void shouldBlockAndUnblockUser() { + + User user = new User( + 1L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + user.setBlocked(true); + + assertTrue(user.isBlocked()); + + user.setBlocked(false); + + assertFalse(user.isBlocked()); + } + + @Test + void shouldChangeUserRole() { + + User user = new User( + 1L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + user.changeRole(Role.ADMIN); + + assertEquals(Role.ADMIN, user.getRole()); + } + + @Test + void shouldAllowSettingIdOnceWhenInitialIdIsZero() { + + User user = new User( + 0L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + user.setId(10L); + + assertEquals(10L, user.getId()); + } + + @Test + void shouldThrowExceptionWhenSettingIdSecondTime() { + + User user = new User( + 1L, + "john", + "hash", + "john@mail.com", + Role.USER + ); + + assertThrows( + IllegalStateException.class, + () -> user.setId(2L) + ); + } +} diff --git a/src/test/java/com/javarush/zyibin/repository/InMemoryTestResultRepositoryTest.java b/src/test/java/com/javarush/zyibin/repository/InMemoryTestResultRepositoryTest.java new file mode 100644 index 0000000..84dbe78 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/repository/InMemoryTestResultRepositoryTest.java @@ -0,0 +1,93 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.TestResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class InMemoryTestResultRepositoryTest { + + private InMemoryTestResultRepository repository; + + @BeforeEach + void setUp() { + repository = new InMemoryTestResultRepository(); + } + + private TestResult createResult(long userId) { + return new TestResult( + userId, + "java-core", + 10, + 7, + true, + LocalDateTime.now() + ); + } + + @Test + void shouldAssignIdWhenSavingResultWithoutId() { + + TestResult result = createResult(1L); + + repository.save(result); + + assertTrue(result.getId() > 0); + } + + @Test + void shouldNotOverrideExistingIdWhenSaving() { + + TestResult result = createResult(1L); + result.setId(42L); + + repository.save(result); + + assertEquals(42L, result.getId()); + } + + @Test + void shouldFindResultsByUserId() { + + TestResult user1Result1 = createResult(1L); + TestResult user1Result2 = createResult(1L); + TestResult user2Result = createResult(2L); + + repository.save(user1Result1); + repository.save(user1Result2); + repository.save(user2Result); + + List results = repository.findByUserId(1L); + + assertEquals(2, results.size()); + assertTrue(results.stream().allMatch(r -> r.getUserId() == 1L)); + } + + @Test + void shouldReturnEmptyListWhenNoResultsForUser() { + + repository.save(createResult(2L)); + + List results = repository.findByUserId(1L); + + assertNotNull(results); + assertTrue(results.isEmpty()); + } + + @Test + void shouldReturnAllSavedResults() { + + repository.save(createResult(1L)); + repository.save(createResult(2L)); + repository.save(createResult(3L)); + + List allResults = repository.findAll(); + + assertEquals(3, allResults.size()); + } + +} diff --git a/src/test/java/com/javarush/zyibin/repository/InMemoryUserRepositoryTest.java b/src/test/java/com/javarush/zyibin/repository/InMemoryUserRepositoryTest.java new file mode 100644 index 0000000..2cdcde3 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/repository/InMemoryUserRepositoryTest.java @@ -0,0 +1,98 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class InMemoryUserRepositoryTest { + + private InMemoryUserRepository repository; + + @BeforeEach + void setUp() { + repository = new InMemoryUserRepository(); + } + + private User createUser(String username) { + return new User( + 0L, + username, + "hash", + username + "@mail.com", + Role.USER + ); + } + + @Test + void shouldAssignIdWhenSavingUserWithoutId() { + + User user = createUser("john"); + + repository.save(user); + + assertTrue(user.getId() > 0); + } + + @Test + void shouldNotOverrideExistingIdWhenSavingUser() { + + User user = createUser("john"); + user.setId(42L); + + repository.save(user); + + assertEquals(42L, user.getId()); + } + + @Test + void shouldFindUserById() { + + User user = createUser("john"); + repository.save(user); + long id = user.getId(); + + Optional found = repository.findById(id); + + assertTrue(found.isPresent()); + assertEquals("john", found.get().getUsername()); + } + + @Test + void shouldFindUserByUsername() { + + User user = createUser("john"); + repository.save(user); + + Optional found = repository.findByUserName("john"); + + assertTrue(found.isPresent()); + assertEquals(user.getId(), found.get().getId()); + } + + @Test + void shouldReturnEmptyOptionalWhenUserNotFoundByUsername() { + + Optional found = repository.findByUserName("unknown"); + + assertTrue(found.isEmpty()); + } + + @Test + void shouldReturnAllSavedUsers() { + + repository.save(createUser("john")); + repository.save(createUser("mary")); + repository.save(createUser("alex")); + + List users = repository.findAll(); + + assertEquals(3, users.size()); + } +} diff --git a/src/test/java/com/javarush/zyibin/repository/QuestionRepositoryTest.java b/src/test/java/com/javarush/zyibin/repository/QuestionRepositoryTest.java new file mode 100644 index 0000000..bb320e0 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/repository/QuestionRepositoryTest.java @@ -0,0 +1,40 @@ +package com.javarush.zyibin.repository; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.source.QuestionSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class QuestionRepositoryTest { + + private QuestionSource questionSource; + private QuestionRepository questionRepository; + + @BeforeEach + void setUp() { + questionSource = mock(QuestionSource.class); + questionRepository = new QuestionRepository(questionSource); + } + + @Test + void shouldLoadQuestionsForGivenTopic() { + Topic topic = Topic.JAVA_CORE; + List expectedQuestions = List.of( + mock(Question.class), + mock(Question.class) + ); + + when(questionSource.loadQuestions(topic)).thenReturn(expectedQuestions); + + List actualQuestions = questionRepository.getQuestions(topic); + + assertEquals(expectedQuestions, actualQuestions); + verify(questionSource, times(1)).loadQuestions(topic); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/AdminUserServiceTest.java b/src/test/java/com/javarush/zyibin/service/AdminUserServiceTest.java new file mode 100644 index 0000000..85855c3 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/AdminUserServiceTest.java @@ -0,0 +1,172 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AdminUserServiceTest { + + @Mock + private UserRepository userRepository; + + private AdminUserService adminUserService; + + @BeforeEach + void setUp() { + adminUserService = new AdminUserService(userRepository); + } + + @Test + void shouldChangeUserRole_whenAdminChangesUserRoleOfOtherUser() { + long adminId = 1L; + long targetUserId = 2L; + Role newRole = Role.USER; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + adminUserService.changeUserRole(adminId, targetUserId, newRole); + + assertEquals(newRole, targetUser.getRole()); + verify(userRepository).findById(targetUserId); + } + + @Test + void shouldNotChangeRole_whenAdminAttemptsToChangeOwnRole() { + long adminId = 1L; + Role newRole = Role.USER; + + User adminUser = new User(adminId, "admin", "hashedpass", "admin@example.com", Role.ADMIN); + + when(userRepository.findById(adminId)).thenReturn(Optional.of(adminUser)); + + adminUserService.changeUserRole(adminId, adminId, newRole); + + assertEquals(Role.ADMIN, adminUser.getRole()); + verify(userRepository).findById(adminId); + } + + @Test + void shouldNotThrowException_whenTargetUserNotFound() { + long adminId = 1L; + long nonExistentUserId = 999L; + Role newRole = Role.USER; + + when(userRepository.findById(nonExistentUserId)).thenReturn(Optional.empty()); + + assertDoesNotThrow(() -> adminUserService.changeUserRole(adminId, nonExistentUserId, newRole)); + + verify(userRepository).findById(nonExistentUserId); + } + + @Test + void shouldChangeToAdminRole_whenRequested() { + long adminId = 1L; + long targetUserId = 2L; + Role newRole = Role.ADMIN; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + targetUser.changeRole(Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + adminUserService.changeUserRole(adminId, targetUserId, newRole); + + assertEquals(Role.ADMIN, targetUser.getRole()); + verify(userRepository).findById(targetUserId); + } + + @Test + void shouldChangeToUserRole_whenRequested() { + long adminId = 1L; + long targetUserId = 2L; + Role newRole = Role.USER; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + targetUser.changeRole(Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + adminUserService.changeUserRole(adminId, targetUserId, newRole); + + assertEquals(Role.USER, targetUser.getRole()); + verify(userRepository).findById(targetUserId); + } + + @Test + void shouldChangeToUserRole_whenRequestedAgain() { + long adminId = 1L; + long targetUserId = 2L; + Role newRole = Role.USER; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + targetUser.changeRole(Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + adminUserService.changeUserRole(adminId, targetUserId, newRole); + + assertEquals(Role.USER, targetUser.getRole()); + verify(userRepository).findById(targetUserId); + } + + @Test + void shouldHandleMultipleRoleChanges() { + long adminId = 1L; + long targetUserId = 2L; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + assertEquals(Role.USER, targetUser.getRole()); + + adminUserService.changeUserRole(adminId, targetUserId, Role.USER); + assertEquals(Role.USER, targetUser.getRole()); + + adminUserService.changeUserRole(adminId, targetUserId, Role.ADMIN); + assertEquals(Role.ADMIN, targetUser.getRole()); + + adminUserService.changeUserRole(adminId, targetUserId, Role.USER); + assertEquals(Role.USER, targetUser.getRole()); + + verify(userRepository, times(3)).findById(targetUserId); + } + + @Test + void shouldPreserveOtherUserProperties_whenChangingRole() { + long adminId = 1L; + long targetUserId = 2L; + Role newRole = Role.USER; + + User targetUser = new User(targetUserId, "targetuser", "hashedpass", "target@example.com", Role.USER); + targetUser.changeRole(Role.USER); + + when(userRepository.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + + String originalUsername = targetUser.getUsername(); + String originalEmail = targetUser.getEmail(); + String originalPasswordHash = targetUser.getPasswordHash(); + + adminUserService.changeUserRole(adminId, targetUserId, newRole); + + assertEquals(originalUsername, targetUser.getUsername()); + assertEquals(originalEmail, targetUser.getEmail()); + assertEquals(originalPasswordHash, targetUser.getPasswordHash()); + assertEquals(newRole, targetUser.getRole()); + + verify(userRepository).findById(targetUserId); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/AuthenticationServiceTest.java b/src/test/java/com/javarush/zyibin/service/AuthenticationServiceTest.java new file mode 100644 index 0000000..bf9a6e6 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/AuthenticationServiceTest.java @@ -0,0 +1,134 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.exception.AuthenticationException; +import com.javarush.zyibin.exception.ValidationException; +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import com.javarush.zyibin.util.PasswordUtil; +import com.javarush.zyibin.validation.UserValidation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuthenticationServiceTest { + + @Mock + private UserRepository userRepository; + + private AuthenticationService authenticationService; + + @BeforeEach + void setUp() { + authenticationService = new AuthenticationService(userRepository); + } + + @Test + void shouldAuthenticateUser_whenCredentialsAreValid() { + String username = "testuser"; + String password = "password123"; + String hashedPassword = PasswordUtil.hashPassword(password); + + User user = new User(1L, username, hashedPassword, "test@example.com", Role.USER); + + when(userRepository.findByUserName(username)).thenReturn(Optional.of(user)); + + User result = authenticationService.authenticate(username, password); + + assertNotNull(result); + assertEquals(username, result.getUsername()); + verify(userRepository).findByUserName(username); + } + + @Test + void shouldThrowAuthenticationException_whenUserNotFound() { + String username = "nonexistent"; + String password = "password123"; + + when(userRepository.findByUserName(username)).thenReturn(Optional.empty()); + + AuthenticationException exception = assertThrows(AuthenticationException.class, + () -> authenticationService.authenticate(username, password)); + + assertEquals("USER_NOT_FOUND", exception.getReason()); + assertEquals("User not found", exception.getMessage()); + verify(userRepository).findByUserName(username); + } + + @Test + void shouldThrowAuthenticationException_whenPasswordIsIncorrect() { + String username = "testuser"; + String correctPassword = "password123"; + String wrongPassword = "wrongpassword"; + String hashedPassword = PasswordUtil.hashPassword(correctPassword); + + User user = new User(1L, username, hashedPassword, "test@example.com", Role.USER); + + when(userRepository.findByUserName(username)).thenReturn(Optional.of(user)); + + AuthenticationException exception = assertThrows(AuthenticationException.class, + () -> authenticationService.authenticate(username, wrongPassword)); + + assertEquals("INVALID_CREDENTIALS", exception.getReason()); + assertEquals("Invalid login or password", exception.getMessage()); + verify(userRepository).findByUserName(username); + } + + @Test + void shouldThrowAuthenticationException_whenUserIsBlocked() { + String username = "blockeduser"; + String password = "password123"; + String hashedPassword = PasswordUtil.hashPassword(password); + + User user = new User(1L, username, hashedPassword, "test@example.com", Role.USER); + user.setBlocked(true); + + when(userRepository.findByUserName(username)).thenReturn(Optional.of(user)); + + AuthenticationException exception = assertThrows(AuthenticationException.class, + () -> authenticationService.authenticate(username, password)); + + assertEquals("USER_BLOCKED", exception.getReason()); + assertEquals("User is blocked", exception.getMessage()); + verify(userRepository).findByUserName(username); + } + + @Test + void shouldPropagateValidationException_whenValidationFails() { + String username = ""; + String password = "password123"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> authenticationService.authenticate(username, password)); + + assertEquals("Username cannot be empty", exception.getMessage()); + assertEquals("username", exception.getField()); + verify(userRepository, never()).findByUserName(any()); + } + + @Test + void shouldAuthenticateAdminUser_whenCredentialsAreValid() { + String username = "admin"; + String password = "adminpass"; + String hashedPassword = PasswordUtil.hashPassword(password); + + User admin = new User(1L, username, hashedPassword, "admin@example.com", Role.ADMIN); + + when(userRepository.findByUserName(username)).thenReturn(Optional.of(admin)); + + User result = authenticationService.authenticate(username, password); + + assertNotNull(result); + assertEquals(username, result.getUsername()); + assertEquals(Role.ADMIN, result.getRole()); + verify(userRepository).findByUserName(username); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/AvatarServiceTest.java b/src/test/java/com/javarush/zyibin/service/AvatarServiceTest.java new file mode 100644 index 0000000..c0375fb --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/AvatarServiceTest.java @@ -0,0 +1,32 @@ +package com.javarush.zyibin.service; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class AvatarServiceTest { + + private final AvatarService avatarService = new AvatarService(); + + @Test + void shouldReturnNonEmptyListOfAvailableAvatars() { + List avatars = avatarService.getAvailableAvatars(); + + assertNotNull(avatars, "Список аватаров не должен быть null"); + assertFalse(avatars.isEmpty(), "Список аватаров не должен быть пустым"); + } + + @Test + void shouldContainExpectedAvatarPaths() { + List avatars = avatarService.getAvailableAvatars(); + + assertTrue(avatars.contains("/resources/avatars/default/avatar1.png")); + assertTrue(avatars.contains("/resources/avatars/default/avatar2.png")); + assertTrue(avatars.contains("/resources/avatars/default/avatar3.png")); + assertTrue(avatars.contains("/resources/avatars/default/avatar4.png")); + assertTrue(avatars.contains("/resources/avatars/default/avatar5.png")); + } +} + diff --git a/src/test/java/com/javarush/zyibin/service/QuestionServiceTest.java b/src/test/java/com/javarush/zyibin/service/QuestionServiceTest.java new file mode 100644 index 0000000..deaf191 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/QuestionServiceTest.java @@ -0,0 +1,212 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.exception.ValidationException; +import com.javarush.zyibin.model.InterviewState; +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import com.javarush.zyibin.validation.QuestionValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class QuestionServiceTest { + + private QuestionService questionService; + private InterviewState interviewState; + private Question currentQuestion; + + @BeforeEach + void setUp() { + questionService = new QuestionService(); + + currentQuestion = new Question( + "What is 2+2?", + List.of("3", "4", "5", "6"), + 1 + ); + + Question nextQuestion = new Question( + "What is 3+3?", + List.of("5", "6", "7", "8"), + 1 + ); + + Topic topic = Topic.JAVA_CORE; + + interviewState = new InterviewState( + Set.of(topic), + List.of(currentQuestion, nextQuestion) + ); + } + + @Test + void shouldIncrementScore_whenCorrectAnswerIsSelected() { + String correctAnswer = "1"; + + questionService.processAnswer(interviewState, correctAnswer); + + assertEquals(1, interviewState.getScore()); + assertEquals(1, interviewState.getCurrentIndex()); + assertFalse(interviewState.isFinished()); + } + + @Test + void shouldNotIncrementScore_whenIncorrectAnswerIsSelected() { + String incorrectAnswer = "0"; + + int initialScore = interviewState.getScore(); + + questionService.processAnswer(interviewState, incorrectAnswer); + + assertEquals(initialScore, interviewState.getScore()); + assertEquals(1, interviewState.getCurrentIndex()); + assertFalse(interviewState.isFinished()); + } + + @Test + void shouldMoveToNextQuestion_afterProcessingAnswer() { + String answer = "1"; + + assertEquals(0, interviewState.getCurrentIndex()); + assertEquals(currentQuestion, interviewState.getCurrentQuestion()); + + questionService.processAnswer(interviewState, answer); + + assertEquals(1, interviewState.getCurrentIndex()); + assertNotEquals(currentQuestion, interviewState.getCurrentQuestion()); + } + + @Test + void shouldFinishInterview_whenLastQuestionIsAnswered() { + String answer = "1"; + + assertEquals(0, interviewState.getCurrentIndex()); + assertFalse(interviewState.isFinished()); + + questionService.processAnswer(interviewState, answer); + + assertEquals(1, interviewState.getCurrentIndex()); + assertFalse(interviewState.isFinished()); + + questionService.processAnswer(interviewState, answer); + + assertEquals(2, interviewState.getCurrentIndex()); + assertTrue(interviewState.isFinished()); + } + + @Test + void shouldPropagateValidationException_whenAnswerIsInvalid() { + String invalidAnswer = "5"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> questionService.processAnswer(interviewState, invalidAnswer)); + + assertEquals("Invalid answer selected", exception.getMessage()); + assertEquals("general", exception.getField()); + + assertEquals(0, interviewState.getCurrentIndex()); + assertEquals(0, interviewState.getScore()); + } + + @Test + void shouldHandleMultipleCorrectAnswers() { + Question question1 = new Question( + "Q1", + List.of("A", "B", "C"), + 0 + ); + Question question2 = new Question( + "Q2", + List.of("X", "Y", "Z"), + 2 + ); + + InterviewState state = new InterviewState( + Set.of(Topic.JUNIT), + List.of(question1, question2) + ); + + questionService.processAnswer(state, "0"); + assertEquals(1, state.getScore()); + assertEquals(1, state.getCurrentIndex()); + + questionService.processAnswer(state, "2"); + assertEquals(2, state.getScore()); + assertEquals(2, state.getCurrentIndex()); + assertTrue(state.isFinished()); + } + + @Test + void shouldHandleAllIncorrectAnswers() { + Question question1 = new Question( + "Q1", + List.of("A", "B", "C"), + 0 + ); + Question question2 = new Question( + "Q2", + List.of("X", "Y", "Z"), + 2 + ); + + InterviewState state = new InterviewState( + Set.of(Topic.JUNIT), + List.of(question1, question2) + ); + + questionService.processAnswer(state, "1"); + assertEquals(0, state.getScore()); + assertEquals(1, state.getCurrentIndex()); + + questionService.processAnswer(state, "1"); + assertEquals(0, state.getScore()); + assertEquals(2, state.getCurrentIndex()); + assertTrue(state.isFinished()); + } + + @Test + void shouldHandleMixedCorrectAndIncorrectAnswers() { + Question question1 = new Question( + "Q1", + List.of("A", "B", "C"), + 1 + ); + Question question2 = new Question( + "Q2", + List.of("X", "Y", "Z"), + 0 + ); + Question question3 = new Question( + "Q3", + List.of("P", "Q", "R"), + 2 + ); + + InterviewState state = new InterviewState( + Set.of(Topic.JUNIT), + List.of(question1, question2, question3) + ); + + questionService.processAnswer(state, "1"); + assertEquals(1, state.getScore()); + assertEquals(1, state.getCurrentIndex()); + + questionService.processAnswer(state, "1"); + assertEquals(1, state.getScore()); + assertEquals(2, state.getCurrentIndex()); + + questionService.processAnswer(state, "2"); + assertEquals(2, state.getScore()); + assertEquals(3, state.getCurrentIndex()); + assertTrue(state.isFinished()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/RegistrationServiceTest.java b/src/test/java/com/javarush/zyibin/service/RegistrationServiceTest.java new file mode 100644 index 0000000..1e3c119 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/RegistrationServiceTest.java @@ -0,0 +1,109 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.exception.ValidationException; +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.validation.UserValidation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RegistrationServiceTest { + + @Mock + private UserService userService; + + private RegistrationService registrationService; + + @BeforeEach + void setUp() { + registrationService = new RegistrationService(userService); + } + + @Test + void shouldRegisterUser_whenDataIsValid() { + String username = "newuser"; + String password = "password123"; + String email = "newuser@example.com"; + + User expectedUser = new User(1L, username, "hashedpassword", email, Role.USER); + + when(userService.register(username, password, email)).thenReturn(expectedUser); + + User result = registrationService.registerUser(username, password, email); + + assertNotNull(result); + assertEquals(username, result.getUsername()); + assertEquals(email, result.getEmail()); + + verify(userService).register(username, password, email); + } + + @Test + void shouldPropagateValidationException_whenValidationFails() { + String username = "ab"; + String password = "123"; + String email = "invalid-email"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> registrationService.registerUser(username, password, email)); + + assertEquals("Username must be at least 3 characters", exception.getMessage()); + assertEquals("username", exception.getField()); + + verify(userService, never()).register(any(), any(), any()); + } + + @Test + void shouldPropagateUserServiceException_whenUserRegistrationFails() { + String username = "existinguser"; + String password = "password123"; + String email = "existing@example.com"; + + when(userService.register(username, password, email)) + .thenThrow(new IllegalStateException("User already exists")); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> registrationService.registerUser(username, password, email)); + + assertEquals("User already exists", exception.getMessage()); + + verify(userService).register(username, password, email); + } + + @Test + void shouldRegisterAdminUser_whenUsernameIsAdmin() { + String username = "admin"; + String password = "adminpass"; + String email = "admin@example.com"; + + User adminUser = new User(1L, username, "hashedadminpass", email, Role.ADMIN); + + when(userService.register(username, password, email)).thenReturn(adminUser); + + User result = registrationService.registerUser(username, password, email); + + assertNotNull(result); + assertEquals(username, result.getUsername()); + assertEquals(email, result.getEmail()); + + verify(userService).register(username, password, email); + } + + @Test + void shouldHandleNullParameters_gracefully() { + ValidationException exception = assertThrows(ValidationException.class, + () -> registrationService.registerUser(null, "password123", "test@example.com")); + + assertEquals("Username cannot be empty", exception.getMessage()); + assertEquals("username", exception.getField()); + + verify(userService, never()).register(any(), any(), any()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserServiceImplTest.java b/src/test/java/com/javarush/zyibin/service/UserServiceImplTest.java new file mode 100644 index 0000000..e8ca67f --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserServiceImplTest.java @@ -0,0 +1,88 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.Role; +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserServiceImplTest { + + private UserRepository userRepository; + private UserServiceImpl userService; + + @BeforeEach + void setUp() { + userRepository = mock(UserRepository.class); + userService = new UserServiceImpl(userRepository); + } + + @Test + void shouldRegisterRegularUser() { + + when(userRepository.findByUserName("john")) + .thenReturn(Optional.empty()); + + User user = userService.register( + "john", + "password123", + "john@mail.com" + ); + + assertEquals("john", user.getUsername()); + assertEquals("john@mail.com", user.getEmail()); + assertEquals(Role.USER, user.getRole()); + + assertNotEquals( + "password123", + user.getPasswordHash(), + "Пароль должен храниться в захешированном виде" + ); + + verify(userRepository, times(1)).save(any(User.class)); + } + + @Test + void shouldRegisterAdminUserWhenUsernameIsAdmin() { + + when(userRepository.findByUserName("admin")) + .thenReturn(Optional.empty()); + + User user = userService.register( + "admin", + "adminPass", + "admin@mail.com" + ); + + assertEquals(Role.ADMIN, user.getRole()); + verify(userRepository).save(any(User.class)); + } + + @Test + void shouldThrowExceptionWhenUserAlreadyExists() { + + when(userRepository.findByUserName("john")) + .thenReturn(Optional.of(mock(User.class))); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> userService.register( + "john", + "password123", + "john@mail.com" + ) + ); + + assertEquals( + "User with this login already exists", + exception.getMessage() + ); + + verify(userRepository, never()).save(any()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserServiceTest.java b/src/test/java/com/javarush/zyibin/service/UserServiceTest.java new file mode 100644 index 0000000..65248c3 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserServiceTest.java @@ -0,0 +1,90 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.model.User; +import com.javarush.zyibin.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + private UserService userService; + + @BeforeEach + void setUp() { + userService = new UserServiceImpl(userRepository); + } + + @Test + void shouldRegisterUser_whenUsernameIsFree() { + + when(userRepository.findByUserName("john")) + .thenReturn(Optional.empty()); + + User user = userService.register( + "john", + "password123", + "john@mail.com" + ); + + assertNotNull(user); + assertEquals("john", user.getUsername()); + assertEquals("john@mail.com", user.getEmail()); + + assertNotEquals("password123", user.getPasswordHash()); + + verify(userRepository).save(any(User.class)); + } + + @Test + void shouldThrowException_whenUsernameAlreadyExists() { + + when(userRepository.findByUserName("john")) + .thenReturn(Optional.of(mock(User.class))); + + assertThrows( + IllegalStateException.class, + () -> userService.register( + "john", + "password123", + "john@mail.com" + ) + ); + + verify(userRepository, never()).save(any()); + } + + @Test + void shouldSaveUserWithHashedPassword() { + + when(userRepository.findByUserName("john")) + .thenReturn(Optional.empty()); + + ArgumentCaptor userCaptor = + ArgumentCaptor.forClass(User.class); + + userService.register( + "john", + "password123", + "john@mail.com" + ); + + verify(userRepository).save(userCaptor.capture()); + User savedUser = userCaptor.getValue(); + + assertNotEquals("password123", savedUser.getPasswordHash()); + assertFalse(savedUser.getPasswordHash().isBlank()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserStatisticsServiceImplTest.java b/src/test/java/com/javarush/zyibin/service/UserStatisticsServiceImplTest.java new file mode 100644 index 0000000..a8ad9da --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserStatisticsServiceImplTest.java @@ -0,0 +1,138 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTopicStats; +import com.javarush.zyibin.model.TestResult; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserStatisticsServiceImplTest { + + private final UserStatisticsService service = + new UserStatisticsServiceImpl(); + + @Test + void shouldReturnEmptyStatsForEmptyResults() { + List stats = + service.calculateUserTopicStats(List.of()); + + assertNotNull(stats); + assertTrue(stats.isEmpty()); + } + + @Test + void shouldCountPassedTestForSingleTopic() { + + TestResult result = new TestResult( + 1L, + "java-core", + 10, + 8, + true, + LocalDateTime.now() + ); + + List stats = + service.calculateUserTopicStats(List.of(result)); + + assertEquals(1, stats.size()); + + UserTopicStats stat = stats.get(0); + assertEquals("Java Core", stat.getTopicDisplayName()); + assertEquals(1, stat.getTotal()); + assertEquals(1, stat.getPassed()); + assertEquals(100, stat.getSuccessRate()); + } + + @Test + void shouldCountFailedTestForSingleTopic() { + + TestResult result = new TestResult( + 1L, + "java-core", + 10, + 3, + false, + LocalDateTime.now() + ); + + List stats = + service.calculateUserTopicStats(List.of(result)); + + UserTopicStats stat = stats.get(0); + assertEquals(1, stat.getTotal()); + assertEquals(0, stat.getPassed()); + assertEquals(0, stat.getSuccessRate()); + } + + @Test + void shouldSplitMixedTestIntoSeparateTopics() { + + TestResult result = new TestResult( + 1L, + "java-core,servlets", + 10, + 9, + true, + LocalDateTime.now() + ); + + List stats = + service.calculateUserTopicStats(List.of(result)); + + assertEquals(2, stats.size()); + + UserTopicStats javaCore = + stats.stream() + .filter(s -> s.getTopicDisplayName().equals("Java Core")) + .findFirst() + .orElseThrow(); + + UserTopicStats servlets = + stats.stream() + .filter(s -> s.getTopicDisplayName().equals("Сервлеты")) + .findFirst() + .orElseThrow(); + + assertEquals(1, javaCore.getTotal()); + assertEquals(1, javaCore.getPassed()); + + assertEquals(1, servlets.getTotal()); + assertEquals(1, servlets.getPassed()); + } + + @Test + void shouldAggregateMultipleResultsForSameTopic() { + + TestResult r1 = new TestResult( + 1L, + "java-core", + 10, + 6, + false, + LocalDateTime.now() + ); + + TestResult r2 = new TestResult( + 1L, + "java-core", + 10, + 9, + true, + LocalDateTime.now() + ); + + List stats = + service.calculateUserTopicStats(List.of(r1, r2)); + + assertEquals(1, stats.size()); + + UserTopicStats stat = stats.get(0); + assertEquals(2, stat.getTotal()); + assertEquals(1, stat.getPassed()); + assertEquals(50, stat.getSuccessRate()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserTestStatisticsServiceTest.java b/src/test/java/com/javarush/zyibin/service/UserTestStatisticsServiceTest.java new file mode 100644 index 0000000..b90fd4f --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserTestStatisticsServiceTest.java @@ -0,0 +1,71 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTestStats; +import com.javarush.zyibin.model.TestResult; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserTestStatisticsServiceTest { + + private final UserTestStatisticsService service = + new UserTestStatisticsService(); + + @Test + void shouldTreatMixedTestAsSingleTest() { + + TestResult result = new TestResult( + 1L, + "java-core,servlets", + + 10, + 8, + true, + LocalDateTime.now() + ); + + List stats = service.calculate(List.of(result)); + + assertEquals(1, stats.size()); + + UserTestStats stat = stats.get(0); + assertEquals("Java Core, Сервлеты", stat.getTestName()); + assertEquals(1, stat.getTotal()); + assertEquals(1, stat.getPassed()); + assertEquals(100, stat.getSuccessRate()); + } + + @Test + void shouldAggregateSameTestRunsTogether() { + TestResult r1 = new TestResult( + 1L, + "java-core,servlets", + 10, + 6, + false, + LocalDateTime.now() + ); + + TestResult r2 = new TestResult( + 1L, + "java-core,servlets", + 10, + 9, + true, + LocalDateTime.now() + ); + + List stats = + service.calculate(List.of(r1, r2)); + + assertEquals(1, stats.size()); + + UserTestStats stat = stats.get(0); + assertEquals(2, stat.getTotal()); + assertEquals(1, stat.getPassed()); + assertEquals(50, stat.getSuccessRate()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserTestStatsTest.java b/src/test/java/com/javarush/zyibin/service/UserTestStatsTest.java new file mode 100644 index 0000000..67ed52b --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserTestStatsTest.java @@ -0,0 +1,62 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTestStats; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserTestStatsTest { + + @Test + void shouldInitializeWithZeroCounters() { + + UserTestStats stats = new UserTestStats("Java Core"); + + assertEquals(0, stats.getTotal()); + assertEquals(0, stats.getPassed()); + assertEquals(0, stats.getSuccessRate()); + } + + @Test + void shouldIncrementTotalCounter() { + + UserTestStats stats = new UserTestStats("Java Core"); + + stats.incrementTotal(); + stats.incrementTotal(); + + assertEquals(2, stats.getTotal()); + } + + @Test + void shouldIncrementPassedCounter() { + + UserTestStats stats = new UserTestStats("Java Core"); + + stats.incrementPassed(); + + assertEquals(1, stats.getPassed()); + } + + @Test + void shouldCalculateSuccessRateCorrectly() { + + UserTestStats stats = new UserTestStats("Java Core"); + + stats.incrementTotal(); // 1 + stats.incrementTotal(); // 2 + stats.incrementTotal(); // 3 + stats.incrementPassed(); // 1 + stats.incrementPassed(); // 2 + + assertEquals(66, stats.getSuccessRate()); + } + + @Test + void shouldReturnZeroSuccessRateWhenTotalIsZero() { + + UserTestStats stats = new UserTestStats("Java Core"); + + assertEquals(0, stats.getSuccessRate()); + } +} diff --git a/src/test/java/com/javarush/zyibin/service/UserTopicStatsTest.java b/src/test/java/com/javarush/zyibin/service/UserTopicStatsTest.java new file mode 100644 index 0000000..15198ef --- /dev/null +++ b/src/test/java/com/javarush/zyibin/service/UserTopicStatsTest.java @@ -0,0 +1,62 @@ +package com.javarush.zyibin.service; + +import com.javarush.zyibin.dto.UserTopicStats; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserTopicStatsTest { + + @Test + void shouldInitializeWithZeroCounters() { + + UserTopicStats stats = new UserTopicStats("Java Core"); + + assertEquals(0, stats.getTotal()); + assertEquals(0, stats.getPassed()); + assertEquals(0, stats.getSuccessRate()); + } + + @Test + void shouldIncrementTotalCounter() { + + UserTopicStats stats = new UserTopicStats("Java Core"); + + stats.incrementTotal(); + stats.incrementTotal(); + + assertEquals(2, stats.getTotal()); + } + + @Test + void shouldIncrementPassedCounter() { + + UserTopicStats stats = new UserTopicStats("Java Core"); + + stats.incrementPassed(); + + assertEquals(1, stats.getPassed()); + } + + @Test + void shouldCalculateSuccessRateCorrectly() { + + UserTopicStats stats = new UserTopicStats("Java Core"); + + stats.incrementTotal(); // 1 + stats.incrementTotal(); // 2 + stats.incrementTotal(); // 3 + stats.incrementPassed(); // 1 + stats.incrementPassed(); // 2 + + assertEquals(66, stats.getSuccessRate()); + } + + @Test + void shouldReturnZeroSuccessRateWhenTotalIsZero() { + + UserTopicStats stats = new UserTopicStats("Java Core"); + + assertEquals(0, stats.getSuccessRate()); + } +} diff --git a/src/test/java/com/javarush/zyibin/session/SessionUtilsTest.java b/src/test/java/com/javarush/zyibin/session/SessionUtilsTest.java new file mode 100644 index 0000000..da4e993 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/session/SessionUtilsTest.java @@ -0,0 +1,83 @@ +package com.javarush.zyibin.session; + +import com.javarush.zyibin.model.InterviewState; +import jakarta.servlet.http.HttpSession; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SessionUtilsTest { + + @Test + void hasInterviewShouldReturnFalseWhenSessionIsNull() { + + boolean result = SessionUtils.hasInterview(null); + + assertFalse(result); + } + + @Test + void hasInterviewShouldReturnFalseWhenNoInterviewStateInSession() { + + HttpSession session = mock(HttpSession.class); + when(session.getAttribute(SessionUtils.INTERVIEW_STATE)) + .thenReturn(null); + + boolean result = SessionUtils.hasInterview(session); + + assertFalse(result); + } + + @Test + void hasInterviewShouldReturnTrueWhenInterviewStateExists() { + + HttpSession session = mock(HttpSession.class); + InterviewState state = mock(InterviewState.class); + + when(session.getAttribute(SessionUtils.INTERVIEW_STATE)) + .thenReturn(state); + + boolean result = SessionUtils.hasInterview(session); + + assertTrue(result); + } + + @Test + void getInterviewStateShouldReturnStateFromSession() { + + HttpSession session = mock(HttpSession.class); + InterviewState state = mock(InterviewState.class); + + when(session.getAttribute(SessionUtils.INTERVIEW_STATE)) + .thenReturn(state); + + InterviewState result = + SessionUtils.getInterviewState(session); + + assertEquals(state, result); + } + + @Test + void setInterviewStateShouldStoreStateInSession() { + + HttpSession session = mock(HttpSession.class); + InterviewState state = mock(InterviewState.class); + + SessionUtils.setInterviewState(session, state); + + verify(session, times(1)) + .setAttribute(SessionUtils.INTERVIEW_STATE, state); + } + + @Test + void clearInterviewShouldRemoveStateFromSession() { + + HttpSession session = mock(HttpSession.class); + + SessionUtils.clearInterview(session); + + verify(session, times(1)) + .removeAttribute(SessionUtils.INTERVIEW_STATE); + } +} diff --git a/src/test/java/com/javarush/zyibin/source/FileQuestionSourceTest.java b/src/test/java/com/javarush/zyibin/source/FileQuestionSourceTest.java new file mode 100644 index 0000000..46f170d --- /dev/null +++ b/src/test/java/com/javarush/zyibin/source/FileQuestionSourceTest.java @@ -0,0 +1,29 @@ +package com.javarush.zyibin.source; + +import com.javarush.zyibin.model.Question; +import com.javarush.zyibin.model.Topic; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class FileQuestionSourceTest { + + private final FileQuestionSource source = new FileQuestionSource(); + + @Test + void shouldLoadQuestionsFromFileForExistingTopic() { + + List questions = source.loadQuestions(Topic.JAVA_CORE); + + assertNotNull(questions); + assertFalse(questions.isEmpty()); + + Question question = questions.get(0); + assertNotNull(question.getQuestionText()); + assertNotNull(question.getAnswers()); + } + +} diff --git a/src/test/java/com/javarush/zyibin/util/PasswordUtilTest.java b/src/test/java/com/javarush/zyibin/util/PasswordUtilTest.java new file mode 100644 index 0000000..cdebb19 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/util/PasswordUtilTest.java @@ -0,0 +1,159 @@ +package com.javarush.zyibin.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PasswordUtilTest { + + @Test + void shouldHashPassword_whenPasswordIsProvided() { + String password = "testPassword123"; + + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + assertTrue(hashedPassword.length() > 0); + } + + @Test + void shouldGenerateConsistentHash_forSamePassword() { + String password = "consistentPassword"; + + String hash1 = PasswordUtil.hashPassword(password); + String hash2 = PasswordUtil.hashPassword(password); + + assertEquals(hash1, hash2); + } + + @Test + void shouldGenerateDifferentHashes_forDifferentPasswords() { + String password1 = "password1"; + String password2 = "password2"; + + String hash1 = PasswordUtil.hashPassword(password1); + String hash2 = PasswordUtil.hashPassword(password2); + + assertNotEquals(hash1, hash2); + } + + @Test + void shouldHandleEmptyPassword() { + String password = ""; + + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + } + + @Test + void shouldHandleSpecialCharacters() { + String password = "p@ssw0rd!#$%^&*()_+-=[]{}|;:,.<>?"; + + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + } + + @Test + void shouldHandleUnicodeCharacters() { + String password = "пароль123"; + + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + } + + @Test + void shouldHandleLongPasswords() { + String password = "a".repeat(1000); + + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + } + + @Test + void shouldGenerateFixedLengthHashes() { + String password1 = "short"; + String password2 = "muchLongerPasswordWithManyCharacters123456789"; + + String hash1 = PasswordUtil.hashPassword(password1); + String hash2 = PasswordUtil.hashPassword(password2); + + assertEquals(hash1.length(), hash2.length()); + assertEquals(64, hash1.length()); + assertEquals(64, hash2.length()); + } + + @Test + void shouldContainOnlyHexCharacters() { + String password = "testPassword"; + String hashedPassword = PasswordUtil.hashPassword(password); + + assertTrue(hashedPassword.matches("[0-9a-fA-F]+")); + } + + @Test + void shouldBeCaseSensitive() { + String password1 = "Password"; + String password2 = "password"; + + String hash1 = PasswordUtil.hashPassword(password1); + String hash2 = PasswordUtil.hashPassword(password2); + + assertNotEquals(hash1, hash2); + } + + @Test + void shouldThrowException_whenUtilityClassInstantiationIsAttempted() { + assertThrows(UnsupportedOperationException.class, () -> { + try { + Class clazz = PasswordUtil.class; + java.lang.reflect.Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + } catch (Exception e) { + if (e.getCause() instanceof UnsupportedOperationException) { + throw (UnsupportedOperationException) e.getCause(); + } + throw new RuntimeException(e); + } + }); + } + + @Test + void shouldHandleCommonPasswords() { + String[] commonPasswords = { + "123456", + "password", + "123456789", + "12345678", + "12345", + "1234567", + "1234567890", + "qwerty", + "abc123", + "password123" + }; + + for (String password : commonPasswords) { + String hashedPassword = PasswordUtil.hashPassword(password); + + assertNotNull(hashedPassword); + assertFalse(hashedPassword.isEmpty()); + assertNotEquals(password, hashedPassword); + assertEquals(64, hashedPassword.length()); + } + } +} diff --git a/src/test/java/com/javarush/zyibin/validation/UserValidationTest.java b/src/test/java/com/javarush/zyibin/validation/UserValidationTest.java new file mode 100644 index 0000000..e28c983 --- /dev/null +++ b/src/test/java/com/javarush/zyibin/validation/UserValidationTest.java @@ -0,0 +1,299 @@ +package com.javarush.zyibin.validation; + +import com.javarush.zyibin.exception.ValidationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UserValidationTest { + + private UserValidation userValidation; + + @BeforeEach + void setUp() { + userValidation = new UserValidation(); + } + + @Test + void shouldValidateRegistration_whenAllDataIsValid() { + String username = "validuser"; + String password = "password123"; + String email = "valid@example.com"; + + assertDoesNotThrow(() -> userValidation.validateRegistration(username, password, email)); + } + + @Test + void shouldValidateLogin_whenDataIsValid() { + String username = "testuser"; + String password = "password123"; + + assertDoesNotThrow(() -> userValidation.validateLogin(username, password)); + } + + @Test + void shouldThrowException_whenLoginUsernameIsEmpty() { + String username = ""; + String password = "password123"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateLogin(username, password)); + + assertEquals("Username cannot be empty", exception.getMessage()); + assertEquals("username", exception.getField()); + assertEquals("USERNAME_INVALID", exception.getErrorCode()); + } + + @Test + void shouldThrowException_whenLoginUsernameIsNull() { + String username = null; + String password = "password123"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateLogin(username, password)); + + assertEquals("Username cannot be empty", exception.getMessage()); + assertEquals("username", exception.getField()); + } + + @Test + void shouldThrowException_whenLoginPasswordIsEmpty() { + String username = "testuser"; + String password = ""; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateLogin(username, password)); + + assertEquals("Password cannot be empty", exception.getMessage()); + assertEquals("password", exception.getField()); + assertEquals("PASSWORD_INVALID", exception.getErrorCode()); + } + + @Test + void shouldThrowException_whenLoginPasswordIsNull() { + String username = "testuser"; + String password = null; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateLogin(username, password)); + + assertEquals("Password cannot be empty", exception.getMessage()); + assertEquals("password", exception.getField()); + } + + @Test + void shouldValidateUsername_whenValid() { + String[] validUsernames = { + "user", + "test123", + "user_name", + "user-name", + "validusername123", + "User_Name" + }; + + for (String username : validUsernames) { + assertDoesNotThrow(() -> userValidation.validateUsername(username)); + } + } + + @Test + void shouldThrowException_whenUsernameIsTooShort() { + String username = "ab"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateUsername(username)); + + assertEquals("Username must be at least 3 characters", exception.getMessage()); + assertEquals("username", exception.getField()); + } + + @Test + void shouldThrowException_whenUsernameIsTooLong() { + String username = "a".repeat(21); + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateUsername(username)); + + assertEquals("Username must be at most 20 characters", exception.getMessage()); + assertEquals("username", exception.getField()); + } + + @Test + void shouldThrowException_whenUsernameContainsInvalidCharacters() { + String[] invalidUsernames = { + "user@name", + "user.name", + "user name", + "user#name", + "user+name" + }; + + for (String username : invalidUsernames) { + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateUsername(username)); + + assertEquals("Username can only contain letters, numbers, underscores and hyphens", exception.getMessage()); + assertEquals("username", exception.getField()); + } + } + + @Test + void shouldValidatePassword_whenValid() { + String[] validPasswords = { + "123456", + "password", + "Password123", + "securepass", + "a".repeat(100) + }; + + for (String password : validPasswords) { + assertDoesNotThrow(() -> userValidation.validatePassword(password)); + } + } + + @Test + void shouldThrowException_whenPasswordIsTooShort() { + String password = "12345"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validatePassword(password)); + + assertEquals("Password must be at least 6 characters", exception.getMessage()); + assertEquals("password", exception.getField()); + } + + @Test + void shouldThrowException_whenPasswordIsTooLong() { + String password = "a".repeat(101); + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validatePassword(password)); + + assertEquals("Password must be no more than 100 characters", exception.getMessage()); + assertEquals("password", exception.getField()); + } + + @Test + void shouldValidateEmail_whenValid() { + String[] validEmails = { + "test@example.com", + "user.name@domain.co.uk", + "user+tag@example.org", + "user123@test-domain.com", + "a@b.co" + }; + + for (String email : validEmails) { + assertDoesNotThrow(() -> userValidation.validateEmail(email)); + } + } + + @Test + void shouldThrowException_whenEmailIsEmpty() { + String email = ""; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateEmail(email)); + + assertEquals("Email cannot be empty", exception.getMessage()); + assertEquals("email", exception.getField()); + } + + @Test + void shouldThrowException_whenEmailIsNull() { + String email = null; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateEmail(email)); + + assertEquals("Email cannot be empty", exception.getMessage()); + assertEquals("email", exception.getField()); + } + + @Test + void shouldThrowException_whenEmailFormatIsInvalid() { + String invalidEmail = "invalid-email"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateEmail(invalidEmail)); + + assertEquals("Invalid email format", exception.getMessage()); + assertEquals("email", exception.getField()); + } + + @Test + void shouldThrowException_whenEmailIsTooLong() { + String email = "user@" + "a".repeat(95) + ".com"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateEmail(email)); + + assertEquals("Email is too long", exception.getMessage()); + assertEquals("email", exception.getField()); + } + + @Test + void shouldValidateProfile_whenDataIsValid() { + String nickname = "Valid Nickname"; + String about = "Valid about section with reasonable length"; + + assertDoesNotThrow(() -> userValidation.validateProfile(nickname, about)); + } + + @Test + void shouldValidateProfile_whenDataIsNull() { + assertDoesNotThrow(() -> userValidation.validateProfile(null, null)); + } + + @Test + void shouldThrowException_whenNicknameIsTooLong() { + String nickname = "a".repeat(51); + String about = "Valid about"; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateProfile(nickname, about)); + + assertEquals("Nickname is too long (max 50 characters)", exception.getMessage()); + assertEquals("general", exception.getField()); + } + + @Test + void shouldThrowException_whenAboutIsTooLong() { + String nickname = "Valid nickname"; + String about = "a".repeat(501); + + ValidationException exception = assertThrows(ValidationException.class, + () -> userValidation.validateProfile(nickname, about)); + + assertEquals("About section is too long (max 500 characters)", exception.getMessage()); + assertEquals("general", exception.getField()); + } + + @Test + void shouldTrimWhitespaceInUsername() { + String username = " validuser "; + + assertDoesNotThrow(() -> userValidation.validateUsername(username)); + } + + @Test + void shouldTrimWhitespaceInEmail() { + String email = " test@example.com "; + + assertDoesNotThrow(() -> userValidation.validateEmail(email)); + } + + @Test + void shouldHandleEdgeCasesInRegistration() { + assertThrows(ValidationException.class, + () -> userValidation.validateRegistration("", "", "")); + + assertThrows(ValidationException.class, + () -> userValidation.validateRegistration("ab", "12345", "invalid")); + + assertDoesNotThrow(() -> userValidation.validateRegistration("valid", "123456", "valid@example.com")); + } +}