S SmartDocs
Chuỗi bài: Angelia java 122 dòng · Cập nhật 2026-09-27

ChatMessage.java

Angelia/Java/Week_6_7/code/08_chat_protocol/ChatMessage.java

/**
 * 聊天協定的「訊息」ADT:用 sealed interface + record 把每一種合法訊息列舉出來。
 *
 * 協定草案(一行一則訊息,欄位以單一空白分隔,最後一個欄位可含空白):
 *
 *   客戶端 → 伺服器                    伺服器 → 客戶端
 *   ---------------------------------  --------------------------------------
 *   LOGIN <name>                        OK [<text>]
 *   JOIN <room>                         ERR <code> <text>
 *   SAY <room> <text...>                MSG <room> <name> <text...>
 *   QUIT                                BYE
 *
 * 這個類別把「文字 ↔ 結構化資料」的轉換集中在一個地方(parse / encode),
 * 伺服器與客戶端的其他程式碼就完全不需要碰字串切割。
 */
public sealed interface ChatMessage
        permits ChatMessage.Login, ChatMessage.Join, ChatMessage.Say, ChatMessage.Quit,
                ChatMessage.Ok, ChatMessage.Err, ChatMessage.Msg, ChatMessage.Bye {

    // ---- 客戶端 → 伺服器 ----
    record Login(String name) implements ChatMessage {}
    record Join(String room) implements ChatMessage {}
    record Say(String room, String text) implements ChatMessage {}
    record Quit() implements ChatMessage {}

    // ---- 伺服器 → 客戶端 ----
    record Ok(String text) implements ChatMessage {}
    record Err(int code, String text) implements ChatMessage {}
    record Msg(String room, String name, String text) implements ChatMessage {}
    record Bye() implements ChatMessage {}

    /** 協定錯誤:收到不符合規格的一行。 */
    class ProtocolException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public ProtocolException(String message) {
            super(message);
        }
    }

    /**
     * 把一行文字解析成 ChatMessage。
     * @throws ProtocolException 若格式不合法
     */
    static ChatMessage parse(String line) {
        if (line == null) {
            throw new ProtocolException("連線已關閉(讀到 null)");
        }
        String[] parts = line.split(" ", 2);            // 先切出指令字
        String command = parts[0].toUpperCase();
        String rest = parts.length > 1 ? parts[1] : "";
        switch (command) {
            case "LOGIN": {
                requireNonEmpty(rest, "LOGIN 需要名字");
                requireNoSpace(rest, "名字不可含空白");
                return new Login(rest);
            }
            case "JOIN": {
                requireNonEmpty(rest, "JOIN 需要房間名");
                requireNoSpace(rest, "房間名不可含空白");
                return new Join(rest);
            }
            case "SAY": {
                String[] p = rest.split(" ", 2);         // <room> <text...>
                if (p.length < 2 || p[0].isEmpty()) {
                    throw new ProtocolException("SAY 需要房間名與內容");
                }
                return new Say(p[0], p[1]);
            }
            case "QUIT": return new Quit();
            case "OK":   return new Ok(rest);
            case "ERR": {
                String[] p = rest.split(" ", 2);
                if (p.length < 2) {
                    throw new ProtocolException("ERR 需要錯誤碼與說明");
                }
                try {
                    return new Err(Integer.parseInt(p[0]), p[1]);
                } catch (NumberFormatException e) {
                    throw new ProtocolException("ERR 的錯誤碼必須是整數:" + p[0]);
                }
            }
            case "MSG": {
                String[] p = rest.split(" ", 3);         // <room> <name> <text...>
                if (p.length < 3) {
                    throw new ProtocolException("MSG 需要房間、名字與內容");
                }
                return new Msg(p[0], p[1], p[2]);
            }
            case "BYE":  return new Bye();
            default:
                throw new ProtocolException("未知的指令:" + parts[0]);
        }
    }

    /** 把 ChatMessage 編碼回協定文字(不含換行)。parse(encode(m)) 必須等於 m。 */
    static String encode(ChatMessage m) {
        // switch 模式比對(Java 21):sealed 讓編譯器確認我們沒有漏掉任何一種訊息
        return switch (m) {
            case Login l -> "LOGIN " + l.name();
            case Join j  -> "JOIN " + j.room();
            case Say s   -> "SAY " + s.room() + " " + s.text();
            case Quit q  -> "QUIT";
            case Ok o    -> o.text().isEmpty() ? "OK" : "OK " + o.text();
            case Err e   -> "ERR " + e.code() + " " + e.text();
            case Msg g   -> "MSG " + g.room() + " " + g.name() + " " + g.text();
            case Bye b   -> "BYE";
        };
    }

    private static void requireNonEmpty(String s, String why) {
        if (s.isEmpty()) {
            throw new ProtocolException(why);
        }
    }

    private static void requireNoSpace(String s, String why) {
        if (s.contains(" ")) {
            throw new ProtocolException(why);
        }
    }
}

Bài viết liên quan