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