diff --git a/src/ru/geekbrains/classes/lesson6r/chatclasses/ChatWindow.java b/src/ru/geekbrains/classes/lesson6r/chatclasses/ChatWindow.java index 6dd0f55..082a82a 100644 --- a/src/ru/geekbrains/classes/lesson6r/chatclasses/ChatWindow.java +++ b/src/ru/geekbrains/classes/lesson6r/chatclasses/ChatWindow.java @@ -3,13 +3,10 @@ import javax.swing.*; import javax.swing.border.TitledBorder; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; +import java.awt.event.*; import java.io.IOException; -public class ChatWindow extends JFrame implements MessageSender { +public class ChatWindow extends JFrame implements MessageSender{ private final DefaultListModel listModel; private JList list; private JScrollPane scrollPane; @@ -85,6 +82,7 @@ public void keyPressed(KeyEvent e) { System.exit(-1); } + setVisible(true); textField.requestFocus(); } diff --git a/src/ru/geekbrains/classes/lesson7/App.java b/src/ru/geekbrains/classes/lesson7/App.java new file mode 100644 index 0000000..484650c --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/App.java @@ -0,0 +1,16 @@ +package ru.geekbrains.classes.lesson7; + +import javax.swing.*; + +public class App { + private static ChatWindow chatWindow; + + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + chatWindow = new ChatWindow(); + } + }); + } +} diff --git a/src/ru/geekbrains/classes/lesson7/ChatServer.java b/src/ru/geekbrains/classes/lesson7/ChatServer.java new file mode 100644 index 0000000..bb11d9c --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/ChatServer.java @@ -0,0 +1,95 @@ +package ru.geekbrains.classes.lesson7; + +import ru.geekbrains.classes.lesson7.auth.AuthService; +import ru.geekbrains.classes.lesson7.auth.AuthServiceImpl; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatServer { + private static final String USER_CONNECTED_PATTERN = "/userconn"; + private static final String USER_DISCONN_PATTERN = "/userdisconn"; + private static final Pattern AUTH_PATTERN = Pattern.compile("^/auth (\\w+) (\\w+)$"); + + private AuthService authService = new AuthServiceImpl(); + private Map clientHandlerMap = Collections.synchronizedMap(new HashMap<>()); + + public static void main(String[] args) { + ChatServer chatServer = new ChatServer(); + chatServer.start(7777); + } + + public void start(int port) { + try (ServerSocket serverSocket = new ServerSocket(port)) { + System.out.println("Server started!"); + while (true) { + Socket socket = serverSocket.accept(); + DataInputStream inp = new DataInputStream(socket.getInputStream()); + DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + System.out.println("New client connected!"); + + try { + String authMessage = inp.readUTF(); + Matcher matcher = AUTH_PATTERN.matcher(authMessage); + if (matcher.matches()) { + String username = matcher.group(1); + String password = matcher.group(2); + if (authService.authUser(username, password)) { + clientHandlerMap.put(username, new ClientHandler(username, socket, this)); + out.writeUTF("/auth successful"); + out.flush(); + broadcastUserConnected(); + System.out.printf("Authorization for user %s successful%n", username); + } else { + System.out.printf("Authorization for user %s failed%n", username); + out.writeUTF("/auth fails"); + out.flush(); + socket.close(); + } + } else { + System.out.printf("Incorrect authorization message %s%n", authMessage); + out.writeUTF("/auth fails"); + out.flush(); + socket.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void sendMessage(String userTo, String userFrom, String msg) { + ClientHandler userToClientHandler = clientHandlerMap.get(userTo); + if (userToClientHandler != null) { + userToClientHandler.sendMessage(userFrom, msg); + } else { + System.out.printf("User %s not found. Message from %s is lost.%n", userTo, userFrom); + } + } + + public List getUserList() { + return new ArrayList<>(clientHandlerMap.keySet()); + } + + public void unsubscribeClient(ClientHandler clientHandler) { + clientHandlerMap.remove(clientHandler.getUsername()); + broadcastUserDisconnected(); + } + + public void broadcastUserConnected() { + // TODO сообщать о том, что конкретный пользователь подключился + } + + public void broadcastUserDisconnected() { + // TODO сообщать о том, что конкретный пользователь отключился + } +} diff --git a/src/ru/geekbrains/classes/lesson7/ChatWindow.java b/src/ru/geekbrains/classes/lesson7/ChatWindow.java new file mode 100644 index 0000000..dd550e3 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/ChatWindow.java @@ -0,0 +1,166 @@ +package ru.geekbrains.classes.lesson7; + +import ru.geekbrains.classes.lesson7.auth.LoginDialog; + +import javax.swing.*; +import javax.swing.border.TitledBorder; +import java.awt.*; +import java.awt.event.*; +import java.io.IOException; + +public class ChatWindow extends JFrame implements MessageSender { + private JTextField textField; + private JButton button; + private JScrollPane scrollPane; + private JList messageList; + private DefaultListModel messageListModel; + private JList userList; + private JPanel panel; + + private Network network; + + public ChatWindow() { + super("Чат"); + initWindow(); + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + messageListModel = new DefaultListModel<>(); + messageList = new JList<>(messageListModel); + messageList.setCellRenderer(new MessageCellRenderer()); + messageList.setBorder(new TitledBorder("Сообщения")); + scrollPane = new JScrollPane(messageList); + + panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(messageList, BorderLayout.SOUTH); + panel.setBackground(messageList.getBackground()); + panel.add(scrollPane, BorderLayout.CENTER); + + userList = new JList<>(); + // TODO добавить класс Model для userList по аналогии с messageListModel + userList.setListData(new String[]{"ivan", "petr", "julia"}); + userList.setPreferredSize(new Dimension(100, 0)); + add(userList, BorderLayout.WEST); + + textField = new JTextField(); + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + String userTo = userList.getSelectedValue(); + String text = textField.getText(); + Message msg = new Message(network.getUsername(), userTo, text.trim()); + submitMessage(msg); + textField.setText(""); + } + } + }); + + button = new JButton("Отправить"); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String userTo = userList.getSelectedValue(); + if (userTo == null) { + JOptionPane.showMessageDialog(ChatWindow.this, + "Не указан получатель", + "Отправка сообщения", + JOptionPane.ERROR_MESSAGE); + return; + } + String text = textField.getText(); + if (text == null || text.trim().isEmpty()) { + JOptionPane.showMessageDialog(ChatWindow.this, + "Нельзя отправить пустое вообщение", + "Отправка сообщения", + JOptionPane.ERROR_MESSAGE); + return; + } + Message msg = new Message(network.getUsername(), userTo, text.trim()); + submitMessage(msg); + textField.setText(""); + textField.requestFocus(); + } + }); + button.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + String userTo = userList.getSelectedValue(); + String text = textField.getText(); + if (text == null || text.trim().isEmpty()) { + JOptionPane.showMessageDialog(ChatWindow.this, + "Нельзя отправить пустое вообщение", + "Отправка сообщения", + JOptionPane.ERROR_MESSAGE); + return; + } + Message msg = new Message(network.getUsername(), userTo, text.trim()); + submitMessage(msg); + textField.setText(""); + textField.requestFocus(); + network.sendMessageToUser(msg); + } + } + }); + + Box row = Box.createHorizontalBox(); + row.add(Box.createHorizontalGlue()); + row.add(textField); + row.add(Box.createHorizontalGlue()); + row.add(button); + row.add(Box.createHorizontalGlue()); + + panel.add(row); + + add(scrollPane, BorderLayout.CENTER); + add(panel, BorderLayout.SOUTH); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + if (network != null) { + network.close(); + } + super.windowClosing(e); + } + }); + + setVisible(true); + textField.requestFocus(); + + try { + network = new Network("localhost", 7777, this); + } catch (IOException e) { + e.printStackTrace(); + } + + LoginDialog loginDialog = new LoginDialog(this, network); + loginDialog.setVisible(true); + + if (!loginDialog.isConnected()) { + System.exit(0); + } + } + + @Override + public void submitMessage(Message msg) { + messageListModel.add(messageListModel.size(), msg); + messageList.ensureIndexIsVisible(messageListModel.size() - 1); + } + + private void initWindow() { + // JFrame.setDefaultLookAndFeelDecorated(true); + int height = (int) getDimension().getHeight(); + int width = (int) getDimension().getWidth(); +// setSize(width / 2, height / 2); + setSize(350, 500); + setLocation(width / 2 - getWidth() / 2, height / 2 - getHeight() / 2); + } + + private Dimension getDimension() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Dimension dimension = new Dimension(toolkit.getScreenSize()); + return dimension; + } +} diff --git a/src/ru/geekbrains/classes/lesson7/ClientHandler.java b/src/ru/geekbrains/classes/lesson7/ClientHandler.java new file mode 100644 index 0000000..67a17be --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/ClientHandler.java @@ -0,0 +1,70 @@ +package ru.geekbrains.classes.lesson7; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ClientHandler { + private static final Pattern MESSAGE_PATTERN = Pattern.compile("^/w (\\w+) (.+)", Pattern.MULTILINE); + private static final String MESSAGE_SEND_PATTERN = "/w %s %s"; + + private final Thread handleThread; + private final DataInputStream inputStream; + private final DataOutputStream outputStream; + private final ChatServer server; + private final String username; + private final Socket socket; + + public ClientHandler(String username, Socket socket, ChatServer server) throws IOException { + this.username = username; + this.socket = socket; + this.server = server; + this.inputStream = new DataInputStream(socket.getInputStream()); + this.outputStream = new DataOutputStream(socket.getOutputStream()); + + this.handleThread = new Thread(new Runnable() { + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + String msg = inputStream.readUTF(); + System.out.printf("Message from user %s: %s%n", username, msg); + + Matcher matcher = MESSAGE_PATTERN.matcher(msg); + if (matcher.matches()) { + String userTo = matcher.group(1); + String message = matcher.group(2); + server.sendMessage(userTo, username, message); + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + System.out.printf("Client %s disconnected%n", username); + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + server.unsubscribeClient(ClientHandler.this); + } + } + }); + handleThread.start(); + } + + public void sendMessage(String userTo, String message){ + try { + outputStream.writeUTF(String.format(MESSAGE_SEND_PATTERN, userTo, message)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public String getUsername() { + return username; + } +} diff --git a/src/ru/geekbrains/classes/lesson7/Message.java b/src/ru/geekbrains/classes/lesson7/Message.java new file mode 100644 index 0000000..c029f70 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/Message.java @@ -0,0 +1,33 @@ +package ru.geekbrains.classes.lesson7; + +import java.time.LocalDate; + +public class Message { + private String userFrom; + private String userTo; + private String text; + private LocalDate date; + + public Message(String userFrom, String userTo, String text) { + this.userFrom = userFrom; + this.userTo = userTo; + this.text = text; + this.date = LocalDate.now(); + } + + public String getUserFrom() { + return userFrom; + } + + public String getUserTo() { + return userTo; + } + + public String getText() { + return text; + } + + public LocalDate getDate() { + return date; + } +} diff --git a/src/ru/geekbrains/classes/lesson7/MessageCellRenderer.java b/src/ru/geekbrains/classes/lesson7/MessageCellRenderer.java new file mode 100644 index 0000000..e8b34e3 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/MessageCellRenderer.java @@ -0,0 +1,32 @@ +package ru.geekbrains.classes.lesson7; + +import javax.swing.*; +import java.awt.*; + +public class MessageCellRenderer extends JPanel implements ListCellRenderer { + + private JLabel userName; + private JLabel message; + + public MessageCellRenderer() { + super(); + setLayout(new BorderLayout()); + userName = new JLabel(); + Font font = userName.getFont(); + userName.setFont(font.deriveFont(Font.BOLD)); + message = new JLabel(); + add(userName, BorderLayout.NORTH); + add(message, BorderLayout.SOUTH); + } + + @Override + public Component getListCellRendererComponent(JList list, Message value, int index, + boolean isSelected, boolean cellHasFocus) { + + setBackground(list.getBackground()); + userName.setOpaque(true); + userName.setText(value.getUserFrom()); + message.setText(value.getText()); + return this; + } +} diff --git a/src/ru/geekbrains/classes/lesson7/MessageSender.java b/src/ru/geekbrains/classes/lesson7/MessageSender.java new file mode 100644 index 0000000..1e508a5 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/MessageSender.java @@ -0,0 +1,5 @@ +package ru.geekbrains.classes.lesson7; + +public interface MessageSender { + void submitMessage(Message msg); +} diff --git a/src/ru/geekbrains/classes/lesson7/Network.java b/src/ru/geekbrains/classes/lesson7/Network.java new file mode 100644 index 0000000..0635117 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/Network.java @@ -0,0 +1,115 @@ +package ru.geekbrains.classes.lesson7; + +import ru.geekbrains.classes.lesson7.auth.AuthException; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Network implements Closeable { + + private static final String AUTH_PATTERN = "/auth %s %s"; + private static final String MESSAGE_SEND_PATTERN = "/w %s %s"; + private static final String USER_LIST_PATTERN = "/userlist"; + private static final Pattern MESSAGE_PATTERN = Pattern.compile("^/w (\\w+) (.+)", Pattern.MULTILINE); + + private Socket socket; + private DataOutputStream output; + private DataInputStream input; + private MessageSender messageSender; + private Thread receiver; + private String username; + private final String hostName; + private final int port; + + public Network(String hostName, int port, MessageSender messageSender) throws IOException { + this.socket = new Socket(hostName, port); + this.hostName = hostName; + this.port = port; +// this.output = new DataOutputStream(socket.getOutputStream()); +// this.input = new DataInputStream(socket.getInputStream()); + this.messageSender = messageSender; + + this.receiver = createReceiverThread(); + } + + private Thread createReceiverThread() { + return new Thread(new Runnable() { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + String text = input.readUTF(); + System.out.println("New message " + text); + Matcher matcher = MESSAGE_PATTERN.matcher(text); + if (matcher.matches()) { + Message msg = new Message(matcher.group(1), username, matcher.group(2)); + messageSender.submitMessage(msg); + } else if (text.startsWith(USER_LIST_PATTERN)) { + // TODO обновить список полключенных пользователей + } + } catch (IOException e) { + e.printStackTrace(); + } + } + System.out.printf("Network connection is closed for user %s%n", username); + } + }); + } + + public void sendMessageToUser(Message message) { + sendMessage(String.format(MESSAGE_SEND_PATTERN, message.getUserTo(), message.getText())); + } + + private void sendMessage(String msg) { + try { + output.writeUTF(msg); + output.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void close() { + try { + socket.close(); + // Нужно подождать пока receiver прервется, затем + receiver.interrupt(); + receiver.join(); + } catch (IOException e) { + System.out.println("Клиентское соединение закрыто"); + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void authorize(String username, String password) { + try { + socket = new Socket(hostName, port); + output = new DataOutputStream(socket.getOutputStream()); + input = new DataInputStream(socket.getInputStream()); + + output.writeUTF(String.format(AUTH_PATTERN, username, password)); + String response = input.readUTF(); + if (response.equals("/auth sucsessful")) { + this.username = username; + receiver.start(); + } else { + throw new AuthException(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public String getUsername() { + return username; + } +} diff --git a/src/ru/geekbrains/classes/lesson7/auth/AuthException.java b/src/ru/geekbrains/classes/lesson7/auth/AuthException.java new file mode 100644 index 0000000..85e0845 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/auth/AuthException.java @@ -0,0 +1,4 @@ +package ru.geekbrains.classes.lesson7.auth; + +public class AuthException extends RuntimeException{ +} diff --git a/src/ru/geekbrains/classes/lesson7/auth/AuthService.java b/src/ru/geekbrains/classes/lesson7/auth/AuthService.java new file mode 100644 index 0000000..bd45c42 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/auth/AuthService.java @@ -0,0 +1,5 @@ +package ru.geekbrains.classes.lesson7.auth; + +public interface AuthService { + boolean authUser(String usrName, String password); +} diff --git a/src/ru/geekbrains/classes/lesson7/auth/AuthServiceImpl.java b/src/ru/geekbrains/classes/lesson7/auth/AuthServiceImpl.java new file mode 100644 index 0000000..2d9ba42 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/auth/AuthServiceImpl.java @@ -0,0 +1,20 @@ +package ru.geekbrains.classes.lesson7.auth; + +import java.util.HashMap; +import java.util.Map; + +public class AuthServiceImpl implements AuthService { + public Map users = new HashMap<>(); + + public AuthServiceImpl() { + users.put("ivan", "123"); + users.put("petr", "345"); + users.put("julia", "789"); + } + + @Override + public boolean authUser(String username, String password) { + String pwd = users.get(username); + return pwd != null && pwd.equals(password); + } +} diff --git a/src/ru/geekbrains/classes/lesson7/auth/LoginDialog.java b/src/ru/geekbrains/classes/lesson7/auth/LoginDialog.java new file mode 100644 index 0000000..d093cc3 --- /dev/null +++ b/src/ru/geekbrains/classes/lesson7/auth/LoginDialog.java @@ -0,0 +1,106 @@ +package ru.geekbrains.classes.lesson7.auth; + +import ru.geekbrains.classes.lesson7.Network; + +import javax.swing.*; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class LoginDialog extends JDialog { + + private JTextField tfUsername; + private JPasswordField pfPassword; + private JLabel lbUsername; + private JLabel lbPassword; + private JButton btnLogin; + private JButton btnCancel; + + private Network network; + private boolean connected; + + public LoginDialog(Frame parent, Network network) { + super(parent, "Login", true); + this.network = network; + this.connected = false; + + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints cs = new GridBagConstraints(); + + cs.fill = GridBagConstraints.HORIZONTAL; + + lbUsername = new JLabel("Username: "); + cs.gridx = 0; + cs.gridy = 0; + cs.gridwidth = 1; + panel.add(lbUsername, cs); + + tfUsername = new JTextField(20); + cs.gridx = 1; + cs.gridy = 0; + cs.gridwidth = 2; + panel.add(tfUsername, cs); + + lbPassword = new JLabel("Password: "); + cs.gridx = 0; + cs.gridy = 1; + cs.gridwidth = 1; + panel.add(lbPassword, cs); + + pfPassword = new JPasswordField(20); + cs.gridx = 1; + cs.gridy = 1; + cs.gridwidth = 2; + panel.add(pfPassword, cs); + panel.setBorder(new LineBorder(Color.GRAY)); + + btnLogin = new JButton("Login"); + btnCancel = new JButton("Cancel"); + + JPanel bp = new JPanel(); + bp.add(btnLogin); + btnLogin.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + try { + network.authorize(tfUsername.getText(), String.valueOf(pfPassword.getPassword())); + connected = true; + } catch (AuthException ex) { + JOptionPane.showMessageDialog(LoginDialog.this, + "Ошибка авторизации", + "Авторизация", + JOptionPane.ERROR_MESSAGE); + return; + } + /*catch (IOException ex) { + JOptionPane.showMessageDialog(LoginDialog.this, + "Ошибка сети", + "Авторизация", + JOptionPane.ERROR_MESSAGE); + return; + }*/ + dispose(); + } + }); + + bp.add(btnCancel); + btnCancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dispose(); + } + }); + + getContentPane().add(panel, BorderLayout.CENTER); + getContentPane().add(bp, BorderLayout.PAGE_END); + + pack(); + setResizable(false); + setLocationRelativeTo(parent); + } + + public boolean isConnected() { + return connected; + } +}