diff --git a/pom.xml b/pom.xml
index cb226e88..c06b5d29 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,16 @@
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.13.0
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.13.0
+
javax.servlet
javax.servlet-api
@@ -28,6 +38,29 @@
jstl
1.2
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.1
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.8.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.11.0
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.12
+
@@ -37,6 +70,24 @@
maven-war-plugin
3.3.2
+
+
+ maven-surefire-plugin
+ 2.19.1
+
+
+ org.junit.platform
+ junit-platform-surefire-provider
+ 1.3.2
+
+
+ org.mockito
+ mockito-core
+ 5.11.0
+ test
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/tictactoe/Field.java b/src/main/java/com/tictactoe/Field.java
index c52d2a0d..580521b1 100644
--- a/src/main/java/com/tictactoe/Field.java
+++ b/src/main/java/com/tictactoe/Field.java
@@ -4,9 +4,12 @@
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
public class Field {
private final Map field;
+ private static final Logger LOGGER = LogManager.getLogger(Field.class);
public Field() {
field = new HashMap<>();
@@ -40,6 +43,7 @@ public List getFieldData() {
}
public Sign checkWin() {
+ LOGGER.info("Checking for winner");
List> winPossibilities = List.of(
List.of(0, 1, 2),
List.of(3, 4, 5),
@@ -53,10 +57,10 @@ public Sign checkWin() {
for (List winPossibility : winPossibilities) {
if (field.get(winPossibility.get(0)) == field.get(winPossibility.get(1))
- && field.get(winPossibility.get(0)) == field.get(winPossibility.get(2))) {
+ && field.get(winPossibility.get(0)) == field.get(winPossibility.get(2))) {
return field.get(winPossibility.get(0));
}
}
return Sign.EMPTY;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/tictactoe/InitServlet.java b/src/main/java/com/tictactoe/InitServlet.java
new file mode 100644
index 00000000..43197182
--- /dev/null
+++ b/src/main/java/com/tictactoe/InitServlet.java
@@ -0,0 +1,36 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.List;
+
+@WebServlet(name = "InitServlet", value = "/start")
+public class InitServlet extends HttpServlet {
+ private static final Logger LOGGER = LogManager.getLogger(InitServlet.class);
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ LOGGER.info("Handling GET request for /start");
+ HttpSession currentSession = req.getSession(true);
+
+ Field field = new Field();
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("field", field);
+ currentSession.setAttribute("data", data);
+
+ LOGGER.info("Forwarding request to /index.jsp");
+ try {
+ getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
+ }catch (ServletException | IOException e) {
+ LOGGER.error("An exception occurred while forwarding request to /index.jsp", e);
+ }
+ }
+}
diff --git a/src/main/java/com/tictactoe/LogicServlet.java b/src/main/java/com/tictactoe/LogicServlet.java
new file mode 100644
index 00000000..df132e01
--- /dev/null
+++ b/src/main/java/com/tictactoe/LogicServlet.java
@@ -0,0 +1,123 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.List;
+
+@WebServlet(name = "LogicServlet", value = "/logic")
+public class LogicServlet extends HttpServlet {
+ private static final Logger LOGGER = LogManager.getLogger(LogicServlet.class);
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
+ LOGGER.info("LogicServlet doGet");
+ HttpSession currentSession = req.getSession();
+ Field currentField = extractField(currentSession);
+
+ int index = getSelectedIndex(req);
+ Sign currentSign = currentField.getField().get(index);
+
+ if (Sign.EMPTY != currentSign) {
+ RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
+ try {
+ dispatcher.forward(req, resp);
+ }catch (ServletException | IOException e) {
+ LOGGER.error("An exception occurred while forwarding to /index.jsp", e);
+ }
+ return;
+ }
+ currentField.getField().put(index, Sign.CROSS);
+ LOGGER.debug("Field updated successfully: {}", currentField.getField());
+ try {
+ if (checkWin(resp, currentSession, currentField)) {
+ return;
+ }
+ }catch (IOException e) {
+ LOGGER.error("An exception occurred while checking winner (cross)", e);
+ }
+
+ int emptyFieldIndex = currentField.getEmptyFieldIndex();
+ if (emptyFieldIndex >= 0) {
+ currentField.getField().put(emptyFieldIndex, Sign.NOUGHT);
+ LOGGER.debug("Field updated successfully: {}", currentField.getField());
+ try {
+ if (checkWin(resp, currentSession, currentField)) {
+ return;
+ }
+ }catch (IOException e) {
+ LOGGER.error("An exception occurred while checking winner (nought)", e);
+ }
+ }else{
+ currentSession.setAttribute("draw", true);
+
+ List data = currentField.getFieldData();
+ currentSession.setAttribute("data", data);
+ try {
+ resp.sendRedirect("/index.jsp");
+ }catch (IOException e) {
+ LOGGER.error("An exception occurred while redirecting to /index.jsp", e);
+ }
+ return;
+ }
+
+ List data = currentField.getFieldData();
+
+ currentSession.setAttribute("data", data);
+ currentSession.setAttribute("field", currentField);
+
+ try {
+ resp.sendRedirect("/index.jsp");
+ }catch (IOException e) {
+ LOGGER.error("An exception occurred while redirecting to /index.jsp", e);
+ }
+ }
+
+ private int getSelectedIndex(HttpServletRequest request) {
+ LOGGER.info("Getting 'click' parameter from request");
+ String click = request.getParameter("click");
+ if (click != null && click.matches("\\d+")) {
+ return Integer.parseInt(click);
+ } else {
+ LOGGER.warn("'click' parameter is not a valid number: {}", click);
+ return 0;
+ }
+ }
+
+ private Field extractField(HttpSession session) {
+ Object field = session.getAttribute("field");
+ LOGGER.info("Extracting 'field' attribute from session");
+ if(Field.class != field.getClass()) {
+ LOGGER.error("Session contains an invalid 'field' attribute: {}", field.getClass());
+ session.invalidate();
+ throw new RuntimeException("Session is broken, try one more time");
+ }
+ return (Field) field;
+ }
+
+ private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
+ Sign winner = field.checkWin();
+ if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
+ currentSession.setAttribute("winner", winner);
+
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("data", data);
+
+ try {
+ response.sendRedirect("/index.jsp");
+ }catch (IOException e){
+ LOGGER.error("IOException occurred while redirecting to /index.jsp", e);
+ }
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/tictactoe/RestartServlet.java b/src/main/java/com/tictactoe/RestartServlet.java
new file mode 100644
index 00000000..309a0b17
--- /dev/null
+++ b/src/main/java/com/tictactoe/RestartServlet.java
@@ -0,0 +1,26 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@WebServlet(name = "RestartServlet", value = "/restart")
+public class RestartServlet extends HttpServlet {
+ private static final Logger LOGGER = LogManager.getLogger(RestartServlet.class);
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ LOGGER.info("Invalidating session and redirecting to /start");
+ try {
+ req.getSession().invalidate();
+ resp.sendRedirect("/start");
+ }catch (IOException e){
+ LOGGER.error("IOException occurred while redirecting to /start", e);
+ }
+ }
+}
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 00000000..a355c8de
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,24 @@
+
+
+ %date %level %logger{10} [%file:%line] %msg%n
+ C:/Moje/project-servlet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp
index 964cc071..3a839e98 100644
--- a/src/main/webapp/index.jsp
+++ b/src/main/webapp/index.jsp
@@ -1,16 +1,65 @@
+<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-
Tic-Tac-Toe
+
+ <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+ ">
Tic-Tac-Toe
+
+
+ | ${data.get(0).getSign()} |
+ ${data.get(1).getSign()} |
+ ${data.get(2).getSign()} |
+
+
+ | ${data.get(3).getSign()} |
+ ${data.get(4).getSign()} |
+ ${data.get(5).getSign()} |
+
+
+ | ${data.get(6).getSign()} |
+ ${data.get(7).getSign()} |
+ ${data.get(8).getSign()} |
+
+
-
diff --git a/src/main/webapp/static/img/cross.png b/src/main/webapp/static/img/cross.png
new file mode 100644
index 00000000..f083637f
Binary files /dev/null and b/src/main/webapp/static/img/cross.png differ
diff --git a/src/main/webapp/static/img/nought.png b/src/main/webapp/static/img/nought.png
new file mode 100644
index 00000000..a719acc0
Binary files /dev/null and b/src/main/webapp/static/img/nought.png differ
diff --git a/src/main/webapp/static/main.css b/src/main/webapp/static/main.css
index e69de29b..33049a9b 100644
--- a/src/main/webapp/static/main.css
+++ b/src/main/webapp/static/main.css
@@ -0,0 +1,11 @@
+td {
+ border: 3px solid black;
+ padding: 10px;
+ border-collapse: separate;
+ margin: 10px;
+ width: 100px;
+ height: 100px;
+ font-size: 50px;
+ text-align: center;
+ empty-cells: show;
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/FieldTest.java b/src/test/java/com/tictactoe/FieldTest.java
new file mode 100644
index 00000000..e11359b3
--- /dev/null
+++ b/src/test/java/com/tictactoe/FieldTest.java
@@ -0,0 +1,136 @@
+package com.tictactoe;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class FieldTest {
+ private Field field;
+
+ @BeforeEach
+ public void setUp() {
+ field = new Field();
+ }
+
+ @Test
+ void testGetFieldNotNull(){
+ assertNotNull(field, "Field can not be null");
+ }
+
+ @Test
+ void testGetFieldSize(){
+ assertEquals(9, field.getField().size());
+ }
+
+ @Test
+ void testGetFieldContent(){
+ for (int i = 0; i < 9; i++) {
+ assertEquals(Sign.EMPTY, field.getField().get(i));
+ }
+ }
+
+ @Test
+ public void testGetEmptyFieldIndexWhenEmptyExists() {
+ assertEquals(0, field.getEmptyFieldIndex());
+ }
+
+ @Test
+ public void testGetEmptyFieldIndexWhenEmptyNotExists() {
+ for (int i = 0; i < 9; i++) {
+ field.getField().put(i, Sign.CROSS);
+ }
+ assertEquals(-1, field.getEmptyFieldIndex());
+ }
+
+ @Test
+ void testGetFieldDataNotNull() {
+ assertNotNull(field.getFieldData(), "Field data can not be null");
+ }
+
+ @Test
+ void testGetFieldDataSize() {
+ assertEquals(9, field.getFieldData().size());
+ }
+
+ @Test
+ void testGetFieldDataContent() {
+ field.getField().put(0, Sign.CROSS);
+ field.getField().put(1, Sign.NOUGHT);
+ field.getField().put(2, Sign.EMPTY);
+ field.getField().put(3, Sign.CROSS);
+ field.getField().put(4, Sign.NOUGHT);
+ field.getField().put(5, Sign.EMPTY);
+ field.getField().put(6, Sign.CROSS);
+ field.getField().put(7, Sign.NOUGHT);
+ field.getField().put(8, Sign.EMPTY);
+
+ List expected = Arrays.asList(Sign.CROSS, Sign.NOUGHT, Sign.EMPTY, Sign.CROSS, Sign.NOUGHT, Sign.EMPTY, Sign.CROSS, Sign.NOUGHT, Sign.EMPTY);
+ List actual = field.getFieldData();
+
+ assertEquals(expected, actual, "The field data should match the expected values");
+ }
+
+ @Test
+ void testHorizontalWin() {
+ field.getField().put(0, Sign.CROSS);
+ field.getField().put(1, Sign.CROSS);
+ field.getField().put(2, Sign.CROSS);
+
+ assertEquals(Sign.CROSS, field.checkWin(), "X should win with a horizontal line");
+ }
+
+ @Test
+ void testVerticalWin() {
+ field.getField().put(0, Sign.NOUGHT);
+ field.getField().put(3, Sign.NOUGHT);
+ field.getField().put(6, Sign.NOUGHT);
+
+ assertEquals(Sign.NOUGHT, field.checkWin(), "O should win with a vertical line");
+ }
+
+ @Test
+ void testDiagonalWin() {
+ field.getField().put(0, Sign.CROSS);
+ field.getField().put(4, Sign.CROSS);
+ field.getField().put(8, Sign.CROSS);
+
+ assertEquals(Sign.CROSS, field.checkWin(), "X should win with a diagonal line");
+ }
+
+ @Test
+ void testNoWinContinueGame() {
+ // Немає виграшної комбінації, гра продовжується
+ field.getField().put(0, Sign.CROSS);
+ field.getField().put(1, Sign.NOUGHT);
+ field.getField().put(2, Sign.CROSS);
+ field.getField().put(3, Sign.CROSS);
+ field.getField().put(4, Sign.NOUGHT);
+ field.getField().put(5, Sign.NOUGHT);
+ field.getField().put(6, Sign.NOUGHT);
+ field.getField().put(7, Sign.CROSS);
+ field.getField().put(8, Sign.CROSS);
+
+ assertEquals(Sign.EMPTY, field.checkWin(), "There should be no winner yet, continue game");
+ }
+
+ @Test
+ void testNoWinDraw() {
+ // Немає виграшної комбінації, гра завершена нічиєю
+ field.getField().put(0, Sign.CROSS);
+ field.getField().put(1, Sign.NOUGHT);
+ field.getField().put(2, Sign.CROSS);
+ field.getField().put(3, Sign.CROSS);
+ field.getField().put(4, Sign.CROSS);
+ field.getField().put(5, Sign.NOUGHT);
+ field.getField().put(6, Sign.NOUGHT);
+ field.getField().put(7, Sign.CROSS);
+ field.getField().put(8, Sign.NOUGHT);
+
+ assertEquals(Sign.EMPTY, field.checkWin(), "The game should end in a draw");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/InitServletTest.java b/src/test/java/com/tictactoe/InitServletTest.java
new file mode 100644
index 00000000..2fef5ced
--- /dev/null
+++ b/src/test/java/com/tictactoe/InitServletTest.java
@@ -0,0 +1,38 @@
+package com.tictactoe;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import static org.mockito.Mockito.*;
+
+class InitServletTest {
+ @Test
+ void testDoGet() throws ServletException, IOException {
+ InitServlet initServlet = new InitServlet();
+
+ ServletConfig config = mock(ServletConfig.class);
+ ServletContext servletContext = mock(ServletContext.class);
+ when(config.getServletContext()).thenReturn(servletContext);
+ initServlet.init(config);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ HttpSession session = mock(HttpSession.class);
+ RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+
+ when(request.getSession(true)).thenReturn(session);
+ when(servletContext.getRequestDispatcher("/index.jsp")).thenReturn(dispatcher);
+
+ initServlet.doGet(request, response);
+
+ verify(session).setAttribute(eq("field"), any(Field.class));
+ verify(session).setAttribute(eq("data"), anyList());
+ verify(dispatcher).forward(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/LogicServletTest.java b/src/test/java/com/tictactoe/LogicServletTest.java
new file mode 100644
index 00000000..77457242
--- /dev/null
+++ b/src/test/java/com/tictactoe/LogicServletTest.java
@@ -0,0 +1,104 @@
+package com.tictactoe;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+public class LogicServletTest {
+ private LogicServlet logicServlet;
+ private HttpServletRequest request;
+ private HttpServletResponse response;
+ private HttpSession session;
+ private Field field;
+ private ServletContext servletContext;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ logicServlet = new LogicServlet();
+
+ ServletConfig config = mock(ServletConfig.class);
+ servletContext = mock(ServletContext.class);
+ when(config.getServletContext()).thenReturn(servletContext);
+ logicServlet.init(config);
+
+ request = mock(HttpServletRequest.class);
+ response = mock(HttpServletResponse.class);
+ session = mock(HttpSession.class);
+ field = mock(Field.class);
+ }
+
+ @Test
+ void testOccupiedCell() throws ServletException, IOException {
+ Map fieldMap = new HashMap<>();
+ fieldMap.put(0, Sign.CROSS);
+ when(field.getField()).thenReturn(fieldMap);
+ when(request.getSession()).thenReturn(session);
+ when(session.getAttribute("field")).thenReturn(field);
+ when(request.getParameter("click")).thenReturn("0");
+
+ RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+ when(servletContext.getRequestDispatcher("/index.jsp")).thenReturn(dispatcher);
+
+ logicServlet.doGet(request, response);
+
+ verify(dispatcher).forward(request, response);
+ }
+
+ @Test
+ void testEmptyCellSetCross() throws ServletException, IOException {
+ Map fieldMap = new HashMap<>();
+ fieldMap.put(0, Sign.EMPTY);
+ when(field.getField()).thenReturn(fieldMap);
+ when(request.getSession()).thenReturn(session);
+ when(session.getAttribute("field")).thenReturn(field);
+ when(request.getParameter("click")).thenReturn("0");
+
+ logicServlet.doGet(request, response);
+
+ verify(field, atLeastOnce()).getField();
+ verify(response).sendRedirect("/index.jsp");
+ }
+ @Test
+ void testInvalidField() {
+ when(request.getSession()).thenReturn(session);
+ when(session.getAttribute("field")).thenReturn("invalid");
+ when(request.getParameter("click")).thenReturn("0");
+
+ assertThrows(RuntimeException.class, () -> logicServlet.doGet(request, response));
+
+ verify(session).invalidate();
+ }
+
+ @Test
+ void testGetSelectedIndex() throws ServletException, IOException {
+ when(request.getSession()).thenReturn(session);
+ when(session.getAttribute("field")).thenReturn(field);
+
+ RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+ when(servletContext.getRequestDispatcher("/index.jsp")).thenReturn(dispatcher);
+
+ when(request.getParameter("click")).thenReturn("5");
+ logicServlet.doGet(request, response);
+ verify(request).getParameter("click");
+
+ when(request.getParameter("click")).thenReturn("abc");
+ logicServlet.doGet(request, response);
+ verify(request, times(2)).getParameter("click");
+
+ when(request.getParameter("click")).thenReturn(null);
+ logicServlet.doGet(request, response);
+ verify(request, times(3)).getParameter("click");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/RestartServletTest.java b/src/test/java/com/tictactoe/RestartServletTest.java
new file mode 100644
index 00000000..e0b26174
--- /dev/null
+++ b/src/test/java/com/tictactoe/RestartServletTest.java
@@ -0,0 +1,28 @@
+package com.tictactoe;
+
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import java.io.IOException;
+import static org.mockito.Mockito.*;
+
+class RestartServletTest {
+
+ @Test
+ void doPostTest() throws IOException {
+ RestartServlet servlet = new RestartServlet();
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ HttpSession session = mock(HttpSession.class);
+
+ when(request.getSession()).thenReturn(session);
+
+ servlet.doPost(request, response);
+
+ verify(session).invalidate();
+ verify(response).sendRedirect("/start");
+ }
+}
\ No newline at end of file