p
This commit is contained in:
parent
0937fc35de
commit
1cb5c0923b
18 changed files with 496 additions and 4 deletions
14
ai-client/build.sh
Executable file
14
ai-client/build.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
echo "Building AI Client..."
|
||||
mvn clean package
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "--------------------------------------------------"
|
||||
echo "Build Successful!"
|
||||
echo "Run AI Client:"
|
||||
echo "java -jar target/ai-client-1.0-SNAPSHOT.jar [host] [port]"
|
||||
echo "--------------------------------------------------"
|
||||
else
|
||||
echo "Build Failed!"
|
||||
exit 1
|
||||
fi
|
||||
35
ai-client/pom.xml
Normal file
35
ai-client/pom.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.lona.tictactoe</groupId>
|
||||
<artifactId>ai-client</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.lona.tictactoe.client.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
406
ai-client/src/main/java/com/lona/tictactoe/client/Main.java
Normal file
406
ai-client/src/main/java/com/lona/tictactoe/client/Main.java
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
package com.lona.tictactoe.client;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Main extends JFrame {
|
||||
private Socket socket;
|
||||
private PrintWriter out;
|
||||
private BufferedReader in;
|
||||
|
||||
private CardLayout cardLayout = new CardLayout();
|
||||
private JPanel mainPanel = new JPanel(cardLayout);
|
||||
|
||||
// Menu Components
|
||||
private JTextField joinCodeField = new JTextField(10);
|
||||
|
||||
// Game Components
|
||||
private JButton[] buttons = new JButton[9];
|
||||
private JLabel statusLabel = new JLabel("Connecting...");
|
||||
private JLabel codeLabel = new JLabel("Code: -");
|
||||
private boolean myTurn = false;
|
||||
private char mySymbol = ' '; // assigned by server implied turn
|
||||
|
||||
public static void main(String[] args) {
|
||||
String host = "38.242.130.81";
|
||||
int port = 1870;
|
||||
|
||||
if (args.length > 0) {
|
||||
host = args[0];
|
||||
}
|
||||
if (args.length > 1) {
|
||||
try {
|
||||
port = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Invalid port number provided, using default 1870");
|
||||
}
|
||||
}
|
||||
|
||||
final String serverHost = host;
|
||||
final int serverPort = port;
|
||||
|
||||
System.out.println("Starting TicTacToe Client...");
|
||||
System.out.println("Target Server: " + serverHost + ":" + serverPort);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
Main client = new Main(serverHost, serverPort);
|
||||
client.setVisible(true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Failed to start GUI: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Instance variables
|
||||
private final String serverHost;
|
||||
private final int serverPort;
|
||||
|
||||
public Main(String host, int port) {
|
||||
super("TicTacToe Client");
|
||||
this.serverHost = host;
|
||||
this.serverPort = port;
|
||||
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(400, 500);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
try {
|
||||
initUI();
|
||||
setContentPane(mainPanel);
|
||||
connectToServer();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void initUI() {
|
||||
// --- MENU PANEL ---
|
||||
JPanel menuPanel = new JPanel(new GridBagLayout());
|
||||
JButton createBtn = new JButton("Create Game");
|
||||
JButton joinBtn = new JButton("Join Game");
|
||||
|
||||
createBtn.addActionListener(e -> send("CREATE"));
|
||||
joinBtn.addActionListener(e -> {
|
||||
String code = joinCodeField.getText().trim();
|
||||
if (!code.isEmpty())
|
||||
send("JOIN " + code);
|
||||
});
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
menuPanel.add(createBtn, gbc);
|
||||
|
||||
gbc.gridy = 1;
|
||||
menuPanel.add(new JLabel("Enter Code:"), gbc);
|
||||
gbc.gridy = 2;
|
||||
menuPanel.add(joinCodeField, gbc);
|
||||
gbc.gridy = 3;
|
||||
menuPanel.add(joinBtn, gbc);
|
||||
|
||||
// --- GAME PANEL ---
|
||||
JPanel gamePanel = new JPanel(new BorderLayout());
|
||||
|
||||
JPanel topPanel = new JPanel(new GridLayout(2, 1));
|
||||
topPanel.add(statusLabel);
|
||||
topPanel.add(codeLabel);
|
||||
gamePanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
JPanel boardPanel = new JPanel(new GridLayout(3, 3));
|
||||
for (int i = 0; i < 9; i++) {
|
||||
int finalI = i;
|
||||
buttons[i] = new JButton("");
|
||||
buttons[i].setFont(new Font("Arial", Font.BOLD, 40));
|
||||
buttons[i].setFocusPainted(false);
|
||||
buttons[i].addActionListener(e -> {
|
||||
int r = finalI / 3;
|
||||
int c = finalI % 3;
|
||||
send("MOVE " + r + " " + c);
|
||||
});
|
||||
boardPanel.add(buttons[i]);
|
||||
}
|
||||
gamePanel.add(boardPanel, BorderLayout.CENTER);
|
||||
|
||||
// --- BUTTONS PANEL ---
|
||||
JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 10, 0));
|
||||
|
||||
JButton leaveBtn = new JButton("Leave Lobby");
|
||||
leaveBtn.setBackground(Color.GRAY);
|
||||
leaveBtn.setForeground(Color.WHITE);
|
||||
leaveBtn.addActionListener(e -> {
|
||||
send("LEAVE");
|
||||
resetGame();
|
||||
});
|
||||
|
||||
JButton surrenderBtn = new JButton("Surrender");
|
||||
surrenderBtn.setBackground(new Color(220, 53, 69)); // Bootstrap Danger Red
|
||||
surrenderBtn.setForeground(Color.WHITE);
|
||||
surrenderBtn.addActionListener(e -> send("SURRENDER"));
|
||||
|
||||
buttonPanel.add(leaveBtn);
|
||||
buttonPanel.add(surrenderBtn);
|
||||
|
||||
gamePanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
mainPanel.add(menuPanel, "MENU");
|
||||
mainPanel.add(gamePanel, "GAME");
|
||||
}
|
||||
|
||||
// AI Configuration
|
||||
private static final String API_KEY = "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";
|
||||
private static final String MODEL = "arcee-ai/trinity-large-preview:free";
|
||||
private char[] boardState = new char[9]; // Keep track of board state
|
||||
|
||||
private void connectToServer() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
System.out.println("Attempting to connect to " + serverHost + ":" + serverPort);
|
||||
socket = new Socket(serverHost, serverPort);
|
||||
out = new PrintWriter(socket.getOutputStream(), true);
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
|
||||
System.out.println("Connected successfully!");
|
||||
SwingUtilities.invokeLater(() -> statusLabel.setText("Connected to Server."));
|
||||
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
System.out.println("Received: " + line); // Log received messages
|
||||
processMessage(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Connection Error: " + e.getMessage());
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Connection Failed.");
|
||||
JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
|
||||
// Don't reset to menu if we can't connect, let user see error
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void send(String msg) {
|
||||
if (out != null)
|
||||
out.println(msg);
|
||||
}
|
||||
|
||||
private void processMessage(String msg) {
|
||||
SwingUtilities.invokeLater(() -> handleMessage(msg));
|
||||
}
|
||||
|
||||
private void handleMessage(String msg) {
|
||||
String[] parts = msg.split(" ");
|
||||
String cmd = parts[0];
|
||||
|
||||
switch (cmd) {
|
||||
case "GAME_CREATED":
|
||||
cardLayout.show(mainPanel, "GAME");
|
||||
if (parts.length > 1) {
|
||||
codeLabel.setText("Code: " + parts[1]);
|
||||
mySymbol = 'X'; // Creator starts as X usually
|
||||
statusLabel.setText("Game Created. Waiting for opponent...");
|
||||
}
|
||||
break;
|
||||
case "JOIN_SUCCESS":
|
||||
cardLayout.show(mainPanel, "GAME");
|
||||
mySymbol = 'O'; // Joiner is O
|
||||
statusLabel.setText("Joined Game.");
|
||||
codeLabel.setText("Code: " + joinCodeField.getText());
|
||||
break;
|
||||
case "START":
|
||||
statusLabel.setText("Game Started! X goes first.");
|
||||
break;
|
||||
case "RESTART":
|
||||
for (int i = 0; i < 9; i++) {
|
||||
buttons[i].setText("");
|
||||
boardState[i] = ' ';
|
||||
}
|
||||
statusLabel.setText("Game Restarted! X goes first.");
|
||||
break;
|
||||
case "BOARD": // BOARD X O X ...
|
||||
String boardStr = msg.substring(6); // remove "BOARD "
|
||||
for (int i = 0; i < 9 && i < boardStr.length(); i++) {
|
||||
char c = boardStr.charAt(i);
|
||||
boardState[i] = c;
|
||||
buttons[i].setText(String.valueOf(c));
|
||||
}
|
||||
break;
|
||||
case "TURN":
|
||||
if (parts.length > 1) {
|
||||
char turn = parts[1].charAt(0);
|
||||
if (turn == mySymbol) {
|
||||
statusLabel.setText("AI Turn (" + mySymbol + ")... Thinking...");
|
||||
makeAiMove();
|
||||
} else {
|
||||
statusLabel.setText("Opponent's Turn (" + turn + ")");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "WIN":
|
||||
String winner = msg.substring(4);
|
||||
statusLabel.setText("Winner: " + winner);
|
||||
// Auto RESTART if AI loses? Or wait for user? Let's just create a dialog but
|
||||
// maybe auto-accept if we want seamless play? No, user requested seamless
|
||||
// winning.
|
||||
// Let's just wait for user to restart.
|
||||
JOptionPane.showMessageDialog(this, "Winner: " + winner);
|
||||
break;
|
||||
case "DRAW":
|
||||
statusLabel.setText("Draw!");
|
||||
JOptionPane.showMessageDialog(this, "Draw!");
|
||||
break;
|
||||
case "OPPONENT_LEFT":
|
||||
JOptionPane.showMessageDialog(this, "Opponent left the game.");
|
||||
resetGame();
|
||||
break;
|
||||
case "ERROR:":
|
||||
if (msg.contains("It is not your turn")) {
|
||||
statusLabel.setText("Not your turn!");
|
||||
} else {
|
||||
// Start thinking again if move was invalid?
|
||||
if (msg.contains("Invalid") || msg.contains("occupied")) {
|
||||
makeAiMove(); // Retry
|
||||
}
|
||||
System.err.println("Server Error: " + msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void makeAiMove() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(2000); // Wait 2s
|
||||
|
||||
String prompt = buildPrompt();
|
||||
System.out.println("Sending Prompt to AI:\n" + prompt);
|
||||
|
||||
String response = callOpenRouter(prompt);
|
||||
System.out.println("AI Response: " + response);
|
||||
|
||||
// Parse response: expecting row, col
|
||||
// The prompt will ask specifically for "row col" format
|
||||
String[] coords = parseCoordinates(response);
|
||||
if (coords != null) {
|
||||
send("MOVE " + coords[0] + " " + coords[1]);
|
||||
} else {
|
||||
System.err.println("Failed to parse AI move. Retrying random...");
|
||||
makeRandomMove();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
makeRandomMove();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void makeRandomMove() {
|
||||
// Fallback: pick first empty spot
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (boardState[i] == ' ') {
|
||||
int r = i / 3;
|
||||
int c = i % 3;
|
||||
send("MOVE " + r + " " + c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildPrompt() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Play Tic-Tac-Toe as '").append(mySymbol).append("'.\n");
|
||||
sb.append("Board:\n");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
char c = boardState[i * 3 + j];
|
||||
sb.append(c == ' ' ? '_' : c).append(" ");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(
|
||||
"Respond ONLY with the coordinates of your next move in JSON format: {\"row\": <0-2>, \"col\": <0-2>}. Do not add any other text.");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String callOpenRouter(String prompt) throws IOException, InterruptedException {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
String jsonBody = "{"
|
||||
+ "\"model\": \"" + MODEL + "\","
|
||||
+ "\"messages\": [{\"role\": \"user\", \"content\": \""
|
||||
+ prompt.replace("\n", "\\n").replace("\"", "\\\"") + "\"}],"
|
||||
+ "\"temperature\": 0.2" // Lower temperature for more deterministic/structured output
|
||||
+ "}";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(java.net.URI.create("https://openrouter.ai/api/v1/chat/completions"))
|
||||
.header("Authorization", "Bearer " + API_KEY)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
String body = response.body();
|
||||
// System.out.println("DEBUG RAW BODY: " + body); // excessive logging, but
|
||||
// helpful if needed
|
||||
|
||||
// Parse content
|
||||
int contentIndex = body.indexOf("\"content\":");
|
||||
if (contentIndex != -1) {
|
||||
int start = body.indexOf("\"", contentIndex + 10) + 1;
|
||||
// Find the end quote, handling escaped quotes
|
||||
int end = start;
|
||||
while (true) {
|
||||
end = body.indexOf("\"", end + 1);
|
||||
if (body.charAt(end - 1) != '\\') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String content = body.substring(start, end);
|
||||
return content.replace("\\n", "\n").replace("\\\"", "\"");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String[] parseCoordinates(String text) {
|
||||
// Look for JSON pattern first: "row": 1, "col": 2
|
||||
Pattern jsonPattern = Pattern.compile("\"row\"\\s*:\\s*(\\d)\\s*,\\s*\"col\"\\s*:\\s*(\\d)");
|
||||
Matcher m = jsonPattern.matcher(text);
|
||||
if (m.find()) {
|
||||
return new String[] { m.group(1), m.group(2) };
|
||||
}
|
||||
|
||||
// Fallback: strictly row col numbers
|
||||
m = Pattern.compile("(\\d)\\s+(\\d)").matcher(text);
|
||||
if (m.find()) {
|
||||
return new String[] { m.group(1), m.group(2) };
|
||||
}
|
||||
|
||||
// One last fallback: just find any two digits
|
||||
m = Pattern.compile("(\\d)[^\\d]+(\\d)").matcher(text);
|
||||
if (m.find()) {
|
||||
return new String[] { m.group(1), m.group(2) };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void resetGame() {
|
||||
cardLayout.show(mainPanel, "MENU");
|
||||
for (JButton btn : buttons)
|
||||
btn.setText("");
|
||||
for (int i = 0; i < 9; i++)
|
||||
boardState[i] = ' ';
|
||||
statusLabel.setText("Connected.");
|
||||
codeLabel.setText("Code: -");
|
||||
}
|
||||
}
|
||||
BIN
ai-client/target/ai-client-1.0-SNAPSHOT.jar
Normal file
BIN
ai-client/target/ai-client-1.0-SNAPSHOT.jar
Normal file
Binary file not shown.
BIN
ai-client/target/classes/com/lona/tictactoe/client/Main.class
Normal file
BIN
ai-client/target/classes/com/lona/tictactoe/client/Main.class
Normal file
Binary file not shown.
5
ai-client/target/maven-archiver/pom.properties
Normal file
5
ai-client/target/maven-archiver/pom.properties
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#Generated by Maven
|
||||
#Tue Feb 10 14:17:06 CET 2026
|
||||
artifactId=ai-client
|
||||
groupId=com.lona.tictactoe
|
||||
version=1.0-SNAPSHOT
|
||||
|
|
@ -0,0 +1 @@
|
|||
com/lona/tictactoe/client/Main.class
|
||||
|
|
@ -0,0 +1 @@
|
|||
/home/collin/tictactoe/ai-client/src/main/java/com/lona/tictactoe/client/Main.java
|
||||
BIN
ai-client/target/original-ai-client-1.0-SNAPSHOT.jar
Normal file
BIN
ai-client/target/original-ai-client-1.0-SNAPSHOT.jar
Normal file
Binary file not shown.
|
|
@ -55,6 +55,7 @@ public class GameInstance {
|
|||
|
||||
if (checkWin(symbol)) {
|
||||
finished = true;
|
||||
lastWinner = symbol;
|
||||
broadcast("WIN " + symbol);
|
||||
} else if (checkDraw()) {
|
||||
finished = true;
|
||||
|
|
@ -72,13 +73,32 @@ public class GameInstance {
|
|||
char loser = (player == playerX) ? 'X' : 'O';
|
||||
char winner = (loser == 'X') ? 'O' : 'X';
|
||||
broadcast("WIN " + winner + " (Surrender)");
|
||||
lastWinner = winner;
|
||||
}
|
||||
|
||||
private char lastWinner = ' ';
|
||||
|
||||
public synchronized void restart() {
|
||||
// Determine who starts: The loser of the previous game.
|
||||
// If lastWinner was 'X', then 'O' (the loser) starts.
|
||||
// If lastWinner was 'O', then 'X' (the loser) starts.
|
||||
// If it was a draw or new game, default to 'X'.
|
||||
|
||||
if (lastWinner == 'X') {
|
||||
currentTurn = 'O';
|
||||
} else if (lastWinner == 'O') {
|
||||
currentTurn = 'X';
|
||||
} else {
|
||||
currentTurn = 'X'; // Default for Draw/New
|
||||
}
|
||||
|
||||
lastWinner = ' '; // Reset for next game
|
||||
finished = false;
|
||||
|
||||
// Clear board
|
||||
for (char[] row : board)
|
||||
Arrays.fill(row, ' ');
|
||||
currentTurn = 'X';
|
||||
finished = false;
|
||||
|
||||
broadcast("RESTART");
|
||||
sendBoard();
|
||||
broadcast("TURN " + currentTurn);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
#Generated by Maven
|
||||
#Tue Feb 10 14:07:14 CET 2026
|
||||
#Tue Feb 10 14:16:42 CET 2026
|
||||
artifactId=server
|
||||
groupId=com.lona.tictactoe
|
||||
version=1.0-SNAPSHOT
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -30,9 +30,11 @@ public class TcpSession extends Thread {
|
|||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
System.out.println("TcpSession: Connecting to " + host + ":" + port);
|
||||
socket = new Socket(host, port);
|
||||
out = new PrintWriter(socket.getOutputStream(), true);
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
System.out.println("TcpSession: Connected successfully");
|
||||
|
||||
// Flush queue
|
||||
String queued;
|
||||
|
|
@ -46,8 +48,16 @@ public class TcpSession extends Thread {
|
|||
session.sendMessage(new TextMessage(line));
|
||||
}
|
||||
}
|
||||
} catch (java.net.ConnectException ce) {
|
||||
System.err.println("TcpSession: Failed to connect to game server: " + ce.getMessage());
|
||||
} catch (java.net.SocketException se) {
|
||||
if (running) {
|
||||
// Only print if we didn't initiate the close
|
||||
System.err.println("TcpSession: Socket closed unexpectedly: " + se.getMessage());
|
||||
se.printStackTrace();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// connection lost or failed
|
||||
System.err.println("TcpSession: IO Error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
close();
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue