Construindo um aplicativo web
- Antes de iniciar, o que estamos construindo?
- Crie um projeto de aplicação web pelo NetBeans
- Prepare o código inicial
- Utilizando o atributo do quadrado
- Adicionando interatividade
- Separação lógica do código
- Estrutura de dados mais adequada
- Modificando uma cópia do tabuleiro
- Maior organização do código
- Adicionando turnos
- Declarando um vencedor
- Fazendo uso de JavaScript para otimizar
- Mudando o método de envio para POST
- Armazenando um histórico
- Reiniciando o jogo
- Código completo do jogo
Antes de iniciar, o que estamos construindo?
Vamos construir um jogo da velha interativo utilizando a tecnologia Java para web.
Crie um projeto de aplicação web pelo NetBeans
Veja como fazer isso em https://netbeans.org/kb/docs/web/quickstart-webapps_pt_BR.html.
Escreva “jogo-da-velha” para o nome do projeto.
Prepare o código inicial
- Remova o arquivo
web/index.html
- Adicione um arquivo
game.css
emweb/
com este código CSS. - Adicione um arquivo
index.jsp
emweb/
com este código JSP. - Crie uma pasta chamada
tags
emweb/WEB-INF/
- Adicione um arquivo
Game.tag
emweb/WEB-INF/tags/
com este código. - Adicione um arquivo
Board.tag
emweb/WEB-INF/tags/
com este código. - Adicione um arquivo
Square.tag
emweb/WEB-INF/tags/
com este código.
Começando
O código inicial contém a estrutura do que estamos construindo. Já contém os estilos de CSS, portanto só precisamos nos preocupar com o Java.
Temos três arquivos com a extensão .tag
dentro da pasta web/WEB-INF/tags/
. Estes são como um JSP, mas funcionam como
componentes, do lado servidor, que podem ser reutilizados:
- Square renderiza um simples
<button>
, representando cada quadrado do jogo. - Board renderiza nove quadrados, representando o tabuleiro do jogo.
- Game renderiza o tabuleiro e as informações do jogo.
Nesse ponto, a interface ainda não está interativa.
Inicie a aplicação
Execute a aplicação no servidor e veja como está sendo renderizado no browser. Verifique o código fonte da página e
perceba que não aparece <t:Board/>
ou <t:Square/>
, isso ocorre porque essas tags são processadas pelo servidor antes
de enviar para o cliente.
Utilizando o atributo do quadrado
Vamos utilizar o atributo value
em Square.tag
para exibir o valor recebido. Faça isso, substituindo o
<!-- A FAZER -->
por ${value}
.
<%-- O conteúdo é especificado aqui --%>
<button class="square">
${value}
</button>
Antes:
Depois: você deverá ver um número em cada quadrado na saída renderizada.
Adicionando interatividade
Vamos fazer cada quadrado ser preenchido com um “X” quando clicar nele.
Recebendo o clique do usuário
Precisamos saber qual quadrado foi clicado. Isso é uma entrada do usuário enviado ao servidor.
Para coletar entradas do usuário, usamos a tag HTML <form>
.
Mas primeiramente, vamos nomear o parâmetro enviado ao servidor, modificando Square.tag
.
<%-- O conteúdo é especificado aqui --%>
<button class="square" name="square" value="${value}">
${value}
</button>
E vamos envolver todos os <button>
em um formulário web.
<%-- O conteúdo é especificado aqui --%>
<form>
<div>
<div class="board-row">
<t:Square value="0" />
<t:Square value="1" />
<t:Square value="2" />
</div>
<div class="board-row">
<t:Square value="3" />
<t:Square value="4" />
<t:Square value="5" />
</div>
<div class="board-row">
<t:Square value="6" />
<t:Square value="7" />
<t:Square value="8" />
</div>
</div>
</form>
Mantendo o estado do tabuleiro
Agora, toda a vez que o usuário clicar em um quadrado, a posição do quadrado será enviado ao servidor. Você pode notar
que a URL do navegador modifica, adicionando ?square=0
, por exemplo.
O servidor recebe o parâmetro, mas não faz nada. Modifique Game.tag
para adicionar uma ação quando o quadrado for clicado.
Esse código irá manter o estado do tabuleiro no servidor, dentro da sessão do usuário.
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
<%-- Variável da sessão requerida --%>
<jsp:useBean id="gameSquares" class="java.util.HashMap" scope="session" />
<%-- Marca no quadrado clicado um X --%>
<c:if test="${param['square'] != null}">
<c:set target="${gameSquares}" property="${param['square']}" value="X"/>
</c:if>
<%-- O conteúdo é especificado aqui --%>
<c:set var="status" value="Próximo jogador: X" />
Para que o <button>
reflita o estado do tabuleiro, modifique o arquivo Square.tag
.
<%-- O conteúdo é especificado aqui --%>
<button class="square" name="square" value="${value}">
${gameSquares[value]}
</button>
Separação lógica do código
O arquivo JSP deveria estar codificado apenas com informações da interface do usuário, pois (1) o responsável por esses arquivos não precisa ser um especialista em Java, (2) ele não precisa saber qual a estrutura de dados sendo usada para gravar os dados e (3) nem mesmo qual o nome do parâmetro do usuário utilizado.
Essas são tarefas de programação e, para isso, usaremos uma classe Java com o código necessário chamado Servlet.
Adicione um Servlet
Crie uma classe Java chamada GameServlet
dentro do pacote tictactoe.web
com este código.
Esta classe deve herdar de HttpServlet
e permite receber requisições do lado cliente pela anotação
@WebServlet(urlPatterns = {"/play-game"})
O GameServlet
, por exemplo, será acessível por http://localhost:8080/jogo-da-velha/play-game
.
Utilize o caminho absoluto nos arquivos estáticos
Se o GameServlet
fosse mapeado para um caminho mais profundo, por exemplo, /jogos/velha
, o arquivo CSS usado em
index.jsp
não seria encontrado. Vamos mudar isso usando um <c:url/>
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jogo da Velha</title>
<link rel="stylesheet" href="<c:url value="/game.css"/>" />
</head>
Mova a lógica do clique para o Servlet
Remova do arquivo Game.tag
as tags <jsp:useBean/>
e <c:if/>
.
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
<%-- O conteúdo é especificado aqui --%>
<c:set var="status" value="Próximo jogador: X" />
E na classe GameServlet
, modifique o método doGet()
para ficar com o seguinte código.
@WebServlet(urlPatterns = {"/play-game"})
public class GameServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// A sessão atual do usuário
HttpSession session = request.getSession();
// Tenta pegar a variável da sessão
Map squares = (Map) session.getAttribute("gameSquares");
// Se a variável não existir, cria uma nova
if (squares == null) {
session.setAttribute("gameSquares", squares = new HashMap());
}
// Marca o quadrado clicado com um X
String paramSquare = request.getParameter("square");
if (paramSquare != null) {
squares.put(paramSquare, 'X');
}
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/index.jsp");
jsp.forward(request, response);
}
}
Configurando um único ponto de entrada
Quando você reiniciar o servidor, a sessão estará limpa, de modo que quando o usuário acessar diretamente o index.jsp
,
a variável da sessão ${gameSquares}
não vai existir. O jogo só irá funcionar se acessado pela URL /play-game
!
Assim, vamos fazer /play-game
como o único ponto de entrada, fazendo primeiramente o seguinte:
- Crie uma pasta chamada
jsp
emweb/WEB-INF/
- Mova o arquivo
index.jsp
paraweb/WEB-INF/jsp/
com o nome degame.jsp
Isso irá esconder o arquivo JSP, pois o usuário não pode acessar qualquer coisa que eseteja dentro da pasta WEB-INF
.
O segundo passo é alterar o GameServlet
para fazer o forward
para o JSP correto:
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
Configure a raiz da aplicação
Este é um passo opcional, mas muito útil.
Como não existe mais um arquivo index.jsp
em web/
, o usuário vai receber um código 404 quando tentar acessar a
aplicação em http://localhost:8080/jogo-da-velha/
.
Vamos resolver isso, crie um novo arquivo index.jsp
com o seguinte conteúdo:
<jsp:forward page="/play-game" />
Estrutura de dados mais adequada
O tipo Map
é genérico demais para armazenar o tabuleiro. De forma a utilizar melhor os recursos do servidor, vamos
modificar o tipo da variável da sessão gameSquares
para um array de caracteres.
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// A sessão atual do usuário
HttpSession session = request.getSession();
// Tenta pegar a variável da sessão
Character[] squares = (Character[]) session.getAttribute("gameSquares");
// Se a variável não existir, cria uma nova
if (squares == null) {
session.setAttribute("gameSquares", squares = new Character[9]);
}
// Marca o quadrado clicado com um X
String paramSquare = request.getParameter("square");
if (paramSquare != null) {
int index = Integer.parseInt(paramSquare);
squares[index] = 'X';
}
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
Modificando uma cópia do tabuleiro
Como o servidor web pode atender a milhares de requisições ao mesmo tempo, é uma boa ideia considerar as variáveis compartilhadas imutáveis, ou seja, trabalhar com cópias dessas variáveis em vez de modificá-las diretamente.
Modifique mais uma vez GameServlet
para satisfazer essa necessidade:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// A sessão atual do usuário
HttpSession session = request.getSession();
// Tenta pegar a variável da sessão
Character[] squares = (Character[]) session.getAttribute("gameSquares");
// Se a variável não existir, cria uma nova
if (squares == null) {
session.setAttribute("gameSquares", squares = new Character[9]);
}
// Marca o quadrado clicado com um X
String paramSquare = request.getParameter("square");
if (paramSquare != null) {
int index = Integer.parseInt(paramSquare);
// Modifica uma cópia do array e o coloca na sessão, no lugar do anterior.
Character[] squaresCopy = squares.clone();
squaresCopy[index] = 'X';
session.setAttribute("gameSquares", squaresCopy);
}
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
Sobre a imutabilidade
Um problema comum na programação web é a concorrência. Como o servidor web pode atender a milhares de requisições ao mesmo tempo, se um dado compartilhado for modificado ao mesmo tempo, sérias corrupções dos dados podem ocorrer.
Existe mais de uma forma de contornar esse problema, uma delas é trabalhar sempre com cópias. Para entender melhor essas questões, faça uma pesquisa sobre threads.
Dois grandes benefícios da imutabilidade
Trabalhar com objetos imutáveis também traz outros benefícios:
- Maior facilidade em implementar desfazer/refazer e funções de máquina do tempo.
- Rastrear mudanças é mais fácil, pois não é preciso fazer comparações.
Maior organização do código
Vamos separar ainda mais o código, criando uma classe GameApp
para gerenciar o jogo. A princípio, essa classe só
mantém um atributo squares
, que é o array de caracteres que representa o tabuleiro.
Crie a classe GameApp
dentro do pacote tictactoe.web
com o seguinte código:
package tictactoe.web;
public class GameApp {
private Character[] squares = new Character[9];
public void clickSquare(int index) {
// Modifica uma cópia do array
Character[] squares = this.squares.clone();
squares[index] = 'X';
// Atualiza o estado do jogo
this.squares = squares;
}
public Character[] getSquares() {
return this.squares;
}
}
Faça as modificações indicadas no código a seguir em GameServlet
. Eis um breve sumário das modificações efetuadas:
- Altera o tipo da variável de sessão para
GameApp
. - Altera o nome dessa variável para apenas
"game"
. - Adiciona um método privado chamado
getGame(HttpServletRequest)
com o código de obter ou criar a variável da sessão. - Chama o método
clickSquare(int)
da classeGameApp
para marcar o “X” do tabuleiro.
@WebServlet(urlPatterns = {"/play-game"})
public class GameServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// O jogo do usuário atual
GameApp game = getGame(request);
// Marca o quadrado clicado com um X
String paramSquare = request.getParameter("square");
if (paramSquare != null) {
int index = Integer.parseInt(paramSquare);
game.clickSquare(index)
}
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
private static GameApp getGame(HttpServletRequest request) {
// A sessão atual do usuário
HttpSession session = request.getSession();
// Tenta pegar a variável da sessão
GameApp game = (GameApp) session.getAttribute("game");
// Se a variável não existir, cria uma nova
if (game == null) {
session.setAttribute("game", game = new GameApp());
}
return game;
}
}
Por último, renomeie o nome da variável em Square.tag
para poder encontrar o tabuleiro:
<%-- O conteúdo é especificado aqui --%>
<button class="square" name="square" value="${value}">
${game.squares[value]}
</button>
Um pequeno refinamento
Crie em GameApp
um método clickSquare(String)
para reduzir a complexidade do servlet.
Faça as seguintes modificações em GameApp
e GameServlet
:
public class GameApp {
private Character[] squares = new Character[9];
public void clickSquare(String param) {
if (param != null) {
int index = Integer.parseInt(param);
clickSquare(index);
}
}
public void clickSquare(int index) {
// Modifica uma cópia do array
Character[] squares = this.squares.clone();
squares[index] = 'X';
// Atualiza o estado do jogo
this.squares = squares;
}
public Character[] getSquares() {
return this.squares;
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// O jogo do usuário atual
GameApp game = getGame(request);
// Marca o quadrado clicado com um X
String paramSquare = request.getParameter("square");
game.clickSquare(paramSquare);
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
Adicionando turnos
Um defeito óbvio do jogo é que somente X pode jogar. Vamos corrigir isso.
Vamos fazer o primeiro movimento ser de ‘X’. Adicione uma nova propriedade à GameApp
:
public class GameApp {
private Character[] squares = new Character[9];
private char turn = 'X';
Lembre-se de adicionar um método get para turn
:
public char getTurn() {
return this.turn;
}
A cada movimento, vamos alternar a propriedade turn
entre os valores ‘X’ e ‘O’. Atualize o método clickSquare(int)
da classe GameApp
para alternar o valor de turn
:
public void clickSquare(int index) {
// Modifica uma cópia do array
Character[] squares = this.squares.clone();
squares[index] = this.turn;
// Atualiza o estado do jogo
this.squares = squares;
this.turn = (this.turn == 'X') ? 'O' : 'X';
}
Agora modifique Game.tag
para usar ${game.turn}
. Assim o usuário saberá de quem é a rodada.
<%-- O conteúdo é especificado aqui --%>
<c:set var="status" value="Próximo jogador: ${game.turn}" />
<div class="game">
<div class="game-board">
Declarando um vencedor
Vamos mostrar quando um jogo foi ganho. Adicione o seguinte método auxiliar ao final de GameApp
:
private static char calculateWinner(Character[] squares) {
// Posições que se marcadas, significa vitória
int[][] lines = new int[][]{
{0, 1, 2},
{3, 4, 5},
{6, 7, 8},
{0, 3, 6},
{1, 4, 7},
{2, 5, 8},
{0, 4, 8},
{2, 4, 6},
};
// Processa o tabuleiro para identificar um vencedor
for (int[] line : lines) {
int a = line[0],
b = line[1],
c = line[2];
if (squares[a] != null && squares[a] == squares[b] && squares[a] == squares[c]) {
return squares[a];
}
}
// Ainda sem um vencedor
return ' ';
}
Adicione uma nova propriedade winner
à classe GameApp
. Essa propriedade poderá ter três valores:
' '
(espaço em branco) indica que o jogo ainda não tem vencedor.'X'
indica que X é o vencedor.'O'
indica que O é o vencedor.
public class GameApp {
private Character[] squares = new Character[9];
private char turn = 'X';
private char winner = ' ';
Também adicione um método get para winner
:
public char getWinner() {
return this.winner;
}
Agora vamos modificar o método clickSquare
para calcular o vencedor usando o método auxiliar:
public void clickSquare(int index) {
// Parando se já tiver um vencedor ou o quadrado já estiver clicado
if (this.winner != ' ' || this.squares[index] != null) {
return;
}
// Modifica uma cópia do array
Character[] squares = this.squares.clone();
squares[index] = this.turn;
// Atualiza o estado do jogo
this.squares = squares;
this.turn = (this.turn == 'X') ? 'O' : 'X';
this.winner = calculateWinner(squares);
}
Para mostrar o vencedor, modifique Game.tag
para definir a mensagem de status correta.
<%-- O conteúdo é especificado aqui --%>
<c:choose>
<c:when test="${game.winner == ' '}">
<c:set var="status" value="Próximo jogador: ${game.turn}" />
</c:when>
<c:otherwise>
<c:set var="status" value="Vencedor: ${game.winner}" />
</c:otherwise>
</c:choose>
<div class="game">
<div class="game-board">
Fazendo uso de JavaScript para otimizar
Quando um quadrado já está marcado ou o jogo já tem um vencedor, não faz mais sentido que o clique gere uma nova requisição ao servidor.
Lembre-se que pode haver milhares de requisições ao mesmo tempo, assim é muito interessante que o navegador só envie requisições para o servidor se realmente necessário.
Configurando para usar jQuery
Vamos utilizar jQuery, uma biblioteca JavaScript, para termos um código JS mais simples.
Adicione ao arquivo web/WEB-INF/jsp/game.jsp
a seguinte tag <script>
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jogo da Velha</title>
<link rel="stylesheet" href="<c:url value="/game.css"/>" />
<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
Impedindo o envio de quadrado já marcado
Crie o arquivo JavaScript web/game.js
(não crie dentro de WEB-INF/
) com o seguinte conteúdo:
// Não submete o formulário se o quadrado clicado já estiver marcado
$('.square').click(function (event) {
if (this.innerText) {
event.preventDefault();
}
});
Agora faça uso desse arquivo, adicionando a seguinte tag <script>
no final de <body>
:
<body>
<t:Game />
<script src="<c:url value="/game.js"/>"></script>
</body>
</html>
Impedindo o envio em jogo com vencedor
Para o quadrado marcado, usamos como referência o valor do innerText
dos elementos <button>
. Já para o
jogo terminado, utilizaremos outra abordagem.
Vamos adicionar um novo atributo HTML à tag <form>
chamado data-locked
, onde:
- Se
<form data-locked="false">
ou não tiver o atributo, o clique estará habilitado. - Se
<form data-locked="true">
, jogo encerrado, qualquer clique estará desabilitado.
Modifique o arquivo Board.tag
para receber o atributo ${locked}
e preencher o atributo HTML.
<%@tag description="O tabuleiro do jogo" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<%-- A lista de atributos dessa tag --%>
<%@attribute name="locked" %>
<%-- Outras tags requeridas para funcionar --%>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
<%-- O conteúdo é especificado aqui --%>
<form data-locked="${locked}">
<div>
<div class="board-row">
<t:Square value="0" />
<t:Square value="1" />
<t:Square value="2" />
</div>
Em seguida, modifique Game.tag
para indicar se o jogo está terminado (locked
) em <t:Board/>
:
<div class="game">
<div class="game-board">
<t:Board locked="${game.winner != ' '}" />
</div>
<div class="game-info">
<div>${status}</div>
<ol><!-- A FAZER --></ol>
</div>
</div>
Se executar a aplicação e olhar o código-fonte no browser, irá perceber que o atributo data-locked
receberá true
quando houver um ganhador, mas ainda não estará bloqueando cliques nos quadrados vazios. Para isso, adicione ao arquivo
JavaScript game.js
o seguinte código:
// Não submete o formulário se o quadrado clicado já estiver marcado
$('.square').click(function (event) {
if (this.innerText) {
event.preventDefault();
}
});
// Não submete o formulário se já houver um vencedor
$('form[data-locked="true"]').submit(function (event) {
event.preventDefault();
});
Mudando o método de envio para POST
Conforme já foi dito, o número do quadrado clicado é enviado adicionando um, por
exemplo, ?square=5
ao final da URL. Isso ocorre porque a tag <form>
está instruída de enviar para o servidor HTTP
utilizando uma requisição GET. Vamos modificar para utilizar uma requisição POST, que envia o parâmetro de uma forma
mais discreta (não adiciona à URL).
Modifique o arquivo Board.tag
para alterar o método de envio do formulário:
<%-- O conteúdo é especificado aqui --%>
<form method="post" data-locked="${locked}">
<div>
<div class="board-row">
<t:Square value="0" />
<t:Square value="1" />
<t:Square value="2" />
</div>
Agora vamos modificar o GameServlet
para receber o clique pelo método POST:
@WebServlet(urlPatterns = {"/play-game"})
public class GameServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Garante que o jogo do usuário exista
getGame(request);
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// O jogo do usuário atual
GameApp game = getGame(request);
// Marca o quadrado clicado
String paramSquare = request.getParameter("square");
game.clickSquare(paramSquare);
// Manda o browser fazer outro pedido, mas com o método GET
response.sendRedirect(".");
}
private static GameApp getGame(HttpServletRequest request) {
Armazenando um histórico
Vamos tornar possível revisitar os estados anteriores do tabuleiro, assim podemos ver como está o jogo depois de alguns movimentos realizados.
Primeiro, em GameApp
, troque o atributo squares
(um array) por history
(uma lista de arrays) e adicione um
construtor para iniciar a lista.
public class GameApp {
private List<Character[]> history = new ArrayList<>(1);
private char turn = 'X';
private char winner = ' ';
public GameApp() {
this.history.add(new Character[9]);
}
Então, modifique o método getSquares()
para que obtenha o tabuleiro do histórico.
public Character[] getSquares() {
return this.history.get(this.history.size() - 1);
}
Em seguida, precisamos modificar o método clickSquare(int)
, já que o estado do jogo agora está estruturado diferente
(usando history
para armazenar o tabuleiro).
public void clickSquare(int index) {
Character[] squares = this.getSquares();
// Parando se já tiver um vencedor ou o quadrado já estiver clicado
if (this.winner != ' ' || squares[index] != null) {
return;
}
// Modifica uma cópia do array
squares = squares.clone();
squares[index] = this.turn;
// Modifica uma cópia do histórico
List<Character[]> history = new ArrayList<>(this.history);
history.add(squares);
// Atualiza o estado do jogo
this.history = history;
this.turn = (this.turn == 'X') ? 'O' : 'X';
this.winner = calculateWinner(squares);
}
Mostrando a lista de movimentos realizados
Modifique o arquivo Game.tag
para exibir uma lista de movimentos realizados.
<div class="game-info">
<div>${status}</div>
<ol>
<li><button>Ir para início do jogo</button></li>
<c:forEach var="move" begin="1" end="${game.historySize - 1}">
<li><button>Ir para movimento #${move}</button></li>
</c:forEach>
</ol>
</div>
</div>
Para funcionar, é preciso adicionar à GameApp
o seguinte método:
public int getHistorySize() {
return this.history.size();
}
Implementando a máquina do tempo
Por enquanto, a lista de movimentos exibe uma sequência de botões que não fazem nada ao clicar, pelos seguintes motivos:
- Os elementos
<button>
não estão devidamente colocados em um<form>
. - Não há uma ação definida no lado servidor para tratar o clique.
Assim, primeiramente modifique Game.tag
para configurar o formulário do histórico:
<div class="game-info">
<div>${status}</div>
<form action="<c:url value="/time-travel"/>" method="post">
<ol>
<li><button name="step" value="0">Ir para início do jogo</button></li>
<c:forEach var="move" begin="1" end="${game.historySize - 1}">
<li><button name="step" value="${move}">Ir para movimento #${move}</button></li>
</c:forEach>
</ol>
</form>
</div>
</div>
Observe que o formulário tem um atributo action
apontando para um caminho inexistente. Se clicarmos nos botões agora,
iremos receber uma página de erro 404. O caminho /time-travel
é outro Servlet, mas ainda não existe na aplicação.
Antes de criarmos esse Servlet, vamos fazer algumas modificações em GameApp
. Adicione o atributo stepNumber
para
manter qual a posição atual do histórico em que o usuário visualiza o jogo.
public class GameApp {
private List<Character[]> history = new ArrayList<>(1);
private int stepNumber = 0;
private char turn = 'X';
private char winner = ' ';
Modifique o método getSquares()
para obter o tabuleiro da posição atual.
public Character[] getSquares() {
return this.history.get(this.stepNumber);
}
Atualize clickSquare(int)
para o histórico ser atualizado em contexto de stepNumber
.
public void clickSquare(int index) {
Character[] squares = this.getSquares();
// Parando se já tiver um vencedor ou o quadrado já estiver clicado
if (this.winner != ' ' || squares[index] != null) {
return;
}
// Modifica uma cópia do array
squares = squares.clone();
squares[index] = this.turn;
// Modifica uma cópia do histórico
List<Character[]> history = new ArrayList<>(this.history.subList(0, this.stepNumber + 1));
history.add(squares);
// Atualiza o estado do jogo
this.history = history;
this.stepNumber += 1;
this.turn = (this.turn == 'X') ? 'O' : 'X';
this.winner = calculateWinner(squares);
}
Depois de tudo, adicione os seguintes métodos à GameApp
. O método jumpTo(int)
faz o trabalho de mover a visualização
para uma nova posição do histórico.
public void jumpTo(String param) {
if (param != null) {
int step = Integer.parseInt(param);
jumpTo(step);
}
}
public void jumpTo(int step) {
this.stepNumber = step;
this.turn = (step % 2 == 0) ? 'X' : 'O';
this.winner = calculateWinner(getSquares());
}
Agora vamos fazer uso desse novo recurso. Crie uma nova classe Java chamada TimeTravelServlet
no pacote tictactoe.web
com este código.
Para o código funcionar, aumente para public
a visibilidade do método getGame
em GameServlet
.
Veja só, criamos um jogo com a funcionalidade de desfazer as jogadas anteriores!
Reiniciando o jogo
Vamos fazer o jogo ser reiniciado quando houver um parâmetro restart=true
.
Para isso, adicione o seguinte método à GameServlet
para invalidar a sessão atual, de forma que na próxima vez que
chamar request.getSession()
, uma nova sessão seja criada.
private static void restartGame(HttpServletRequest request) {
HttpSession session = request.getSession();
session.invalidate();
}
Modifique o GameServlet
para utilizar o novo método ao receber o parâmetro restart=true
. Com essa modificação, se
você acessar http://localhost:8080/jogo-da-velha/?restart=true
.
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Reinicia o jogo se solicitado
String paramRestart = request.getParameter("restart");
if ("true".equals(paramRestart)) {
restartGame(request);
}
// Garante que o jogo do usuário exista
getGame(request);
// Passa a requisição para outro componente
RequestDispatcher jsp = request.getRequestDispatcher("/WEB-INF/jsp/game.jsp");
jsp.forward(request, response);
}
Para facilitar a vida do usuário, mostre, após o primeiro movimento, um link para reiniciar o jogo.
<div class="game-info">
<div>${status}</div>
<form action="<c:url value="/time-travel"/>" method="post">
<ol>
<li><button name="step" value="0">Ir para início do jogo</button></li>
<c:forEach var="move" begin="1" end="${game.historySize - 1}">
<li><button name="step" value="${move}">Ir para movimento #${move}</button></li>
</c:forEach>
</ol>
</form>
<c:if test="${game.historySize > 1}">
<div><a href="?restart=true">Reiniciar jogo</a></div>
</c:if>
</div>
</div>
Fim da sessão por tempo esgotado
Vamos configurar a aplicação para finalizar a sessão por falta de atividade.
Para isso, crie a classe Java SessionTimeoutListener
no pacote tictactoe.web.listeners
:
@WebListener
public class SessionTimeoutListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
se.getSession().setMaxInactiveInterval(5 * 60);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
}
}
Sobre a sessão web
A sessão web é uma estrutura de dados multivalorada mantida pelo servidor de aplicações web (Tomcat, GlassFish, JBoss, etc).
O servidor web sabe qual é a sessão atual do cliente através do uso de cookies. No caso dos servidores em Java, o nome
do cookie é JSESSIONID
.
Esse processo funciona em resumo da seguinte forma:
-
O cliente envia uma requisição, por exemplo:
GET /jogo-da-velha/ HTTP/1.1 Host: localhost:8080 Accept: text/html Accept-Language: pt-BR Cookie: JSESSIONID=eaa2b18d71fad827f5f902c5a735; theme=blue
-
O servidor verifica se a requisição tem um cookie de sessão, se não tiver, uma nova sessão será criada. Na requisição acima, foi passada a sessão
eaa2b18d71fad827f5f902c5a735
, caso essa sessão seja inválida, isto é, não exista dentro da estrutura do servidor web, então, uma nova é retornada na resposta:HTTP/1.1 200 OK Date: Mon, 19 Mar 2018 14:44:14 GMT Content-Type: text/html;charset=UTF-8 Set-Cookie: JSESSIONID=384cdaeb4ddd17e22471a79f95eb
-
O cliente então armazena o cookie
JSESSIONID
com o valor384cdaeb4ddd17e22471a79f95eb
e a partir das próximas requisições enviará esse valor (até receber um novo do servidor).
Quando a sessão é invalidada?
A sessão pode deixar de ser válida pelas seguintes razões:
- Por tempo de inatividade, que é o que fizemos ao adicionar o listener.
- Ao reagir à alguma requisição do usuário, o que ocorre ao passarmos
restart=true
. - A sessão é removida manualmente (e.g. ao reiniciar o servidor).
Código completo do jogo
Você pode baixar todo o código aqui.
Mas veja que não é um projeto NetBeans, você precisa copiar os arquivos para um projeto existente.