diff --git a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java index 6b186facf46..e7a7f3181a4 100644 --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java @@ -1,99 +1,632 @@ -/* - * Copyright 2015 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package io.grpc.examples.helloworld; import io.grpc.Channel; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; -import io.grpc.StatusRuntimeException; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.Logger; +import java.util.Map; +import java.util.HashMap; + +// Part 3 TCP imports (no new files, implement TCP client in this file) +import java.net.Socket; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +// interactive imports +import java.util.Scanner; -/** - * A simple client that requests a greeting from the {@link HelloWorldServer}. - */ public class HelloWorldClient { private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName()); - private final GreeterGrpc.GreeterBlockingStub blockingStub; + private final String tcpHost; + private final int tcpPort; - /** Construct client for accessing HelloWorld server using the existing channel. */ - public HelloWorldClient(Channel channel) { - // 'channel' here is a Channel, not a ManagedChannel, so it is not this code's responsibility to - // shut it down. + private final RestaurantGrpc.RestaurantBlockingStub restaurantStub; - // Passing Channels to code makes code easier to test and makes it easier to reuse Channels. - blockingStub = GreeterGrpc.newBlockingStub(channel); + private String tcpToken = ""; + private String tcpRole = ""; + private String tcpOrderId = ""; + private String tcpTicketId = ""; + + /** Construct client for accessing server using the existing channel. */ + public HelloWorldClient(Channel channel, String tcpHost, int tcpPort) { + restaurantStub = RestaurantGrpc.newBlockingStub(channel); + this.tcpHost = tcpHost; + this.tcpPort = tcpPort; } - /** Say hello to server. */ - public void greet(String name) { - logger.info("Will try to greet " + name + " ..."); - HelloRequest request = HelloRequest.newBuilder().setName(name).build(); - HelloReply response; - try { - response = blockingStub.sayHello(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); + public void runInteractive() { + Scanner sc = new Scanner(System.in); + + while (true) { + System.out.println(); + System.out.println("Interactive Restaurant Client"); + System.out.println("1) TCP Interactive"); + System.out.println("0) Exit"); + System.out.print("> "); + + String choice = sc.nextLine().trim(); + if (choice.equals("0")) break; + + if (choice.equals("1")) { + try { + tcpInteractive(sc); + } catch (Exception e) { + System.out.println("[TCP] Error: " + e.getMessage()); + } + } else { + System.out.println("Invalid option."); + } + } + + System.out.println("Goodbye."); + } + + // TCP Interactive + + private void tcpInteractive(Scanner sc) throws Exception { + String host = tcpHost; + int port = tcpPort; + + // reset session state each time you enter interactive TCP + tcpToken = ""; + tcpRole = ""; + tcpOrderId = ""; + tcpTicketId = ""; + + try (TcpClient c = new TcpClient(host, port)) { + System.out.println("[TCP] " + c.readLine()); // CONNECTED + + while (true) { + System.out.println(); + System.out.println("------ MENU ------"); + System.out.println("Session: token=" + (tcpToken.isEmpty() ? "(none)" : tcpToken) + + " role=" + (tcpRole.isEmpty() ? "(none)" : tcpRole) + + " orderId=" + (tcpOrderId.isEmpty() ? "(none)" : tcpOrderId) + + " ticketId=" + (tcpTicketId.isEmpty() ? "(none)" : tcpTicketId)); + System.out.println("1) Login"); + System.out.println("2) Logout"); + System.out.println("3) List Menu"); + System.out.println("4) Create Order"); + System.out.println("5) Add Item"); + System.out.println("6) Submit Order"); + System.out.println("7) Get Tickets"); + System.out.println("8) Ack Ticket"); + System.out.println("9) Get Bill"); + System.out.println("10) Update Menu Item (MANAGER)"); + System.out.println("0) Back"); + System.out.print("> "); + + String cmd = sc.nextLine().trim(); + if (cmd.equals("0")) { + c.sendLine("QUIT|"); + System.out.println("[TCP] " + c.readLine()); + return; + } + + switch (cmd) { + case "1": tcpLoginInteractive(sc, c); break; + case "2": tcpLogoutInteractive(c); break; + case "3": tcpListMenuInteractive(c); break; + case "4": tcpCreateOrderInteractive(sc, c); break; + case "5": tcpAddItemInteractive(sc, c); break; + case "6": tcpSubmitOrderInteractive(c); break; + case "7": tcpGetTicketsInteractive(sc, c); break; + case "8": tcpAckTicketInteractive(sc, c); break; + case "9": tcpGetBillInteractive(c); break; + case "10": tcpUpdateMenuItemInteractive(sc, c); break; + default: System.out.println("Invalid option."); break; + } + } + } + } + + private void tcpLoginInteractive(Scanner sc, TcpClient c) throws Exception { + System.out.print("username (manager/server/chef): "); + String u = sc.nextLine().trim(); + System.out.print("password: "); + String pass = sc.nextLine().trim(); + + c.sendLine("LOGIN|username=" + u + ";password=" + pass); + String r = c.readLine(); + System.out.println("[TCP] " + r); + + tcpToken = extract(r, "token"); + tcpRole = extract(r, "role"); + tcpOrderId = ""; + tcpTicketId = ""; + } + + private void tcpLogoutInteractive(TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Not logged in."); return; } + c.sendLine("LOGOUT|token=" + tcpToken); + String r = c.readLine(); + System.out.println("[TCP] " + r); + tcpToken = ""; + tcpRole = ""; + tcpOrderId = ""; + tcpTicketId = ""; + } + + private void tcpUpdateMenuItemInteractive(Scanner sc, TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + if (!"MANAGER".equalsIgnoreCase(tcpRole)) { + System.out.println("Only MANAGER can update menu."); return; } - logger.info("Greeting: " + response.getMessage()); + + System.out.print("itemId: "); + String itemId = sc.nextLine().trim(); + + System.out.print("name (blank=no change): "); + String name = sc.nextLine().trim(); + + System.out.print("category STARTER/MAIN/DESSERT/DRINK (blank=no change): "); + String cat = sc.nextLine().trim(); + + Long newPriceCents = null; + System.out.print("change price? (y/N): "); + String changePrice = sc.nextLine().trim(); + if (changePrice.equalsIgnoreCase("y") || changePrice.equalsIgnoreCase("yes")) { + long dollars = readLong(sc, "dollars (>=0): ", 0, Long.MAX_VALUE); + long cents = readLong(sc, "cents (0-99): ", 0, 99); + newPriceCents = dollars * 100 + cents; + } + + System.out.print("active true/false (blank=no change): "); + String active = sc.nextLine().trim(); + + StringBuilder sb = new StringBuilder(); + sb.append("UPDATE_MENU_ITEM|token=").append(tcpToken) + .append(";itemId=").append(itemId); + + if (!name.isEmpty()) sb.append(";name=").append(name); + if (!cat.isEmpty()) sb.append(";category=").append(cat); + if (newPriceCents != null) sb.append(";price=").append(newPriceCents); + if (!active.isEmpty()) sb.append(";active=").append(active); + + c.sendLine(sb.toString()); + System.out.println("[TCP] " + c.readLine()); } - /** - * Greet server. If provided, the first element of {@code args} is the name to use in the - * greeting. The second argument is the target server. - */ - public static void main(String[] args) throws Exception { - String user = "world"; - // Access a service running on the local machine on port 50051 - String target = "localhost:50051"; - // Allow passing in the user and target strings as command line arguments - if (args.length > 0) { - if ("--help".equals(args[0])) { - System.err.println("Usage: [name [target]]"); - System.err.println(""); - System.err.println(" name The name you wish to be greeted by. Defaults to " + user); - System.err.println(" target The server to connect to. Defaults to " + target); - System.exit(1); + private void tcpListMenuInteractive(TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + + c.sendLine("LIST_MENU|token=" + tcpToken); + String r = c.readLine(); + + if (r == null) { + System.out.println("[TCP] No response."); + return; + } + if (!r.startsWith("OK|")) { + System.out.println("[TCP] " + r); + return; + } + + String itemsPart = extract(r, "items"); + if (itemsPart.isEmpty()) { + System.out.println("[TCP] Menu empty."); + return; + } + + class TcpMenuItem { + String id, name, cat; + long cents; + TcpMenuItem(String id, String name, String cat, long cents) { + this.id = id; this.name = name; this.cat = cat; this.cents = cents; + } + } + + java.util.Map> byCat = new java.util.HashMap<>(); + long maxId = "ITEM ID".length(); + long maxName = "NAME".length(); + + String[] items = itemsPart.split(","); + for (String item : items) { + String[] p = item.split("\\^"); + if (p.length != 4) continue; + + String id = p[0].trim(); + String name = p[1].trim(); + String cat = p[2].trim(); + long cents = 0; + try { cents = Long.parseLong(p[3].trim()); } catch (Exception ignored) {} + + byCat.computeIfAbsent(cat, k -> new java.util.ArrayList<>()) + .add(new TcpMenuItem(id, name, cat, cents)); + + if (id.length() > maxId) maxId = id.length(); + if (name.length() > maxName) maxName = name.length(); + } + + String[] order = new String[] { "STARTER", "MAIN", "DESSERT", "DRINK" }; + + System.out.println(); + System.out.println("========== TCP MENU =========="); + + for (String cat : order) { + java.util.List list = byCat.get(cat); + if (list == null || list.isEmpty()) continue; + + list.sort((a, b) -> a.name.compareToIgnoreCase(b.name)); + + System.out.println(); + System.out.println(cat); + System.out.println(repeat('=', cat.length())); + + System.out.printf(" [%-" + (int)maxId + "s] %-" + (int)maxName + "s %s%n", + "ITEM ID", "NAME", "PRICE"); + System.out.println(" " + repeat('-', (int)(maxId + 2)) + + " " + repeat('-', (int)maxName) + + " " + repeat('-', 10)); + + for (TcpMenuItem mi : list) { + String price = centsToMoney(mi.cents); + System.out.printf(" [%-" + (int)maxId + "s] %-" + (int)maxName + "s %10s%n", + mi.id, mi.name, price); } - user = args[0]; } - if (args.length > 1) { - target = args[1]; + + System.out.println(); + } + + private static String centsToMoney(long cents) { + long dollars = cents / 100; + long rem = Math.abs(cents % 100); + return "$" + dollars + "." + (rem < 10 ? "0" + rem : "" + rem); + } + + private void tcpCreateOrderInteractive(Scanner sc, TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + + System.out.print("Type (1=dine-in, 2=takeout): "); + String t = sc.nextLine().trim(); + + if (t.equals("1")) { + System.out.print("table (1-50): "); + String table = sc.nextLine().trim(); + System.out.print("guests (1-20): "); + String guests = sc.nextLine().trim(); + + c.sendLine("CREATE_ORDER|token=" + tcpToken + ";type=DINE_IN;table=" + table + ";guests=" + guests); + } else if (t.equals("2")) { + System.out.print("guest name: "); + String name = sc.nextLine().trim(); + c.sendLine("CREATE_ORDER|token=" + tcpToken + ";type=TAKEOUT;guest=" + name); + } else { + System.out.println("Invalid type."); + return; + } + + String r = c.readLine(); + System.out.println("[TCP] " + r); + tcpOrderId = extract(r, "orderId"); + tcpTicketId = ""; + } + + private void tcpAddItemInteractive(Scanner sc, TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + if (tcpOrderId.isEmpty()) { System.out.println("Create an order first."); return; } + + System.out.print("itemId: "); + String itemId = sc.nextLine().trim(); + System.out.print("qty: "); + String qty = sc.nextLine().trim(); + + c.sendLine("ADD_ITEM|token=" + tcpToken + ";orderId=" + tcpOrderId + ";itemId=" + itemId + ";qty=" + qty); + System.out.println("[TCP] " + c.readLine()); + } + + private void tcpSubmitOrderInteractive(TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + if (tcpOrderId.isEmpty()) { System.out.println("Create an order first."); return; } + + c.sendLine("SUBMIT_ORDER|token=" + tcpToken + ";orderId=" + tcpOrderId); + String r = c.readLine(); + System.out.println("[TCP] " + r); + tcpTicketId = extract(r, "ticketId"); + } + + private void tcpGetTicketsInteractive(Scanner sc, TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + + System.out.print("status (TICKET_SENT / TICKET_ACKNOWLEDGED): "); + String s = sc.nextLine().trim(); + if (s.isEmpty()) s = "TICKET_SENT"; + + c.sendLine("GET_TICKETS|token=" + tcpToken + ";status=" + s); + System.out.println("[TCP] " + c.readLine()); + } + + private void tcpAckTicketInteractive(Scanner sc, TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + + System.out.print("ticketId (blank to use last=" + (tcpTicketId.isEmpty() ? "none" : tcpTicketId) + "): "); + String t = sc.nextLine().trim(); + if (t.isEmpty()) t = tcpTicketId; + if (t.isEmpty()) { System.out.println("No ticketId available."); return; } + + c.sendLine("ACK_TICKET|token=" + tcpToken + ";ticketId=" + t); + System.out.println("[TCP] " + c.readLine()); + } + + private void tcpGetBillInteractive(TcpClient c) throws Exception { + if (tcpToken.isEmpty()) { System.out.println("Login first."); return; } + if (tcpOrderId.isEmpty()) { System.out.println("No orderId in session."); return; } + + c.sendLine("GET_BILL|token=" + tcpToken + ";orderId=" + tcpOrderId); + String r = c.readLine(); + if (r == null) { + System.out.println("[TCP] (no response)"); + return; + } + + printTcpBill(r); + } + + private static void printTcpBill(String line) { + if (!line.startsWith("OK|")) { + System.out.println("[TCP] " + line); + return; + } + + Map kv = parseTcpKv(line); + + String orderId = kv.getOrDefault("orderId", "(unknown)"); + String subtotal = kv.getOrDefault("subtotal", "(unknown)"); + String total = kv.getOrDefault("total", "(unknown)"); + String lines = kv.getOrDefault("lines", ""); + + System.out.println(); + System.out.println(" BILL "); + System.out.println("Order: " + orderId); + + if (lines.trim().isEmpty()) { + System.out.println("(no line items)"); + } else { + int nameW = "ITEM".length(); + int qtyW = "QTY".length(); + int amtW = "AMOUNT".length(); + + String[] items = lines.split(","); + for (String it : items) { + String[] f = it.split("\\^"); + if (f.length < 4) continue; + String name = f[1]; + String qty = f[2]; + String amt = centsToMoneyStringSafe(f[3]); + nameW = Math.max(nameW, name.length()); + qtyW = Math.max(qtyW, qty.length()); + amtW = Math.max(amtW, amt.length()); + } + + System.out.printf("%-" + nameW + "s %" + + qtyW + "s %" + + amtW + "s%n", "ITEM", "QTY", "AMOUNT"); + System.out.println(repeat('-', nameW) + " " + repeat('-', qtyW) + " " + repeat('-', amtW)); + + for (String it : items) { + String[] f = it.split("\\^"); + if (f.length < 4) continue; + + String name = f[1]; + String qty = f[2]; + String amt = centsToMoneyStringSafe(f[3]); + + System.out.printf("%-" + nameW + "s %" + + qtyW + "s %" + + amtW + "s%n", name, qty, amt); + } + } + + System.out.println("Subtotal: " + subtotal); + System.out.println("Total: " + total); + System.out.println(); + } + + private static Map parseTcpKv(String line) { + Map map = new HashMap<>(); + int pipe = line.indexOf('|'); + if (pipe < 0) return map; + + String body = line.substring(pipe + 1); + String[] pairs = body.split(";"); + for (String p : pairs) { + int eq = p.indexOf('='); + if (eq < 0) continue; + String k = p.substring(0, eq).trim(); + String v = p.substring(eq + 1).trim(); + map.put(k, v); + } + return map; + } + + private static String centsToMoneyStringSafe(String centsStr) { + long cents; + try { + cents = Long.parseLong(centsStr.trim()); + } catch (Exception e) { + return centsStr; + } + long dollars = cents / 100; + long rem = Math.abs(cents % 100); + return "USD " + dollars + "." + (rem < 10 ? "0" + rem : "" + rem); + } + + private static String repeat(char ch, int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) sb.append(ch); + return sb.toString(); + } + + private static String moneyToString(Money m) { + long cents = m.getCents(); + long dollars = cents / 100; + long rem = Math.abs(cents % 100); + return m.getCurrency() + " " + dollars + "." + (rem < 10 ? "0" + rem : "" + rem); + } + + private static void printMenu(Menu menu) { + java.util.Map> byCat = new java.util.HashMap<>(); + for (MenuItem mi : menu.getItemsList()) { + if (!mi.getActive()) continue; + byCat.computeIfAbsent(mi.getCategory(), k -> new java.util.ArrayList<>()).add(mi); + } + + MenuCategory[] order = new MenuCategory[] { + MenuCategory.STARTER, MenuCategory.MAIN, MenuCategory.DESSERT, MenuCategory.DRINK + }; + + int idW = "ITEM ID".length(); + int nameW = "NAME".length(); + for (MenuCategory c : order) { + java.util.List items = byCat.get(c); + if (items == null) continue; + for (MenuItem mi : items) { + idW = Math.max(idW, mi.getItemId().length()); + nameW = Math.max(nameW, mi.getName().length()); + } + } + + java.util.function.BiFunction rep = (ch, n) -> { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) sb.append(ch); + return sb.toString(); + }; + + String title = "RESTAURANT MENU"; + int totalW = Math.max(title.length() + 6, idW + 2 + nameW + 2 + 12 + 6); + System.out.println(); + System.out.println("+" + rep.apply('-', totalW) + "+"); + System.out.println("| " + title + rep.apply(' ', Math.max(0, totalW - (title.length() + 2))) + "|"); + System.out.println("+" + rep.apply('-', totalW) + "+"); + + for (MenuCategory cat : order) { + java.util.List items = byCat.get(cat); + if (items == null || items.isEmpty()) continue; + + items.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); + + String catLabel = cat.name(); + System.out.println(); + System.out.println(catLabel); + System.out.println(rep.apply('=', catLabel.length())); + + String header = String.format(" %-"+idW+"s %-"+nameW+"s %s", "ITEM ID", "NAME", "PRICE"); + System.out.println(header); + System.out.println(" " + rep.apply('-', idW) + " " + rep.apply('-', nameW) + " " + rep.apply('-', 10)); + + for (MenuItem mi : items) { + String price = moneyToString(mi.getPrice()); + System.out.printf(" %-"+idW+"s %-"+nameW+"s %10s%n", + mi.getItemId(), mi.getName(), price); + } + } + + System.out.println(); + } + + private static void printBill(Bill bill) { + System.out.println("BILL for " + bill.getOrderId()); + for (OrderLineItem li : bill.getLinesList()) { + System.out.println(" " + li.getQuantity() + "x " + li.getName() + + " @ " + moneyToString(li.getUnitPrice()) + + " = " + moneyToString(li.getLineTotal())); + } + System.out.println(" Subtotal: " + moneyToString(bill.getSubtotal())); + System.out.println(" Total: " + moneyToString(bill.getTotal())); + } + + private static long promptMoneyCents(Scanner sc) { + long dollars = readLong(sc, "dollars (>=0): ", 0, Long.MAX_VALUE); + long cents = readLong(sc, "cents (0-99): ", 0, 99); + return dollars * 100 + cents; + } + + private static long readLong(Scanner sc, String prompt, long min, long max) { + while (true) { + System.out.print(prompt); + String s = sc.nextLine().trim(); + try { + long v = Long.parseLong(s); + if (v < min || v > max) { + System.out.println("Enter a value in range [" + min + ", " + max + "]."); + continue; + } + return v; + } catch (Exception e) { + System.out.println("Enter a valid whole number."); + } + } + } + + static class TcpClient implements AutoCloseable { + private final Socket socket; + private final BufferedReader in; + private final PrintWriter out; + + + TcpClient(String host, int port) throws Exception { + socket = new Socket(host, port); + in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true); + } + + String readLine() throws Exception { return in.readLine(); } + void sendLine(String line) { out.println(line); } + + @Override + public void close() throws Exception { socket.close(); } + } + + private static String extract(String line, String key) { + if (line == null) return ""; + int pipe = line.indexOf('|'); + if (pipe < 0) return ""; + String body = line.substring(pipe + 1); + String[] pairs = body.split(";"); + for (String p : pairs) { + int eq = p.indexOf('='); + if (eq < 0) continue; + String k = p.substring(0, eq).trim(); + String v = p.substring(eq + 1).trim(); + if (k.equals(key)) return v; + } + return ""; + } + + public static void main(String[] args) throws Exception { + String grpcTarget = "localhost:50051"; + String tcpHost = "localhost"; + int tcpPort = 50052; + + if (args.length >= 1) { + grpcTarget =args[0]; + } + if (args.length >= 2) { + tcpHost = args[1]; + } + if (args.length >= 3) { + try { + tcpPort = Integer.parseInt(args[2]); + } catch (Exception e) { + System.out.println("Invalid TCP port: " + args[2] + ". Defaulting to 50052"); + tcpPort = 50052; + } } - // Create a communication channel to the server, known as a Channel. Channels are thread-safe - // and reusable. It is common to create channels at the beginning of your application and reuse - // them until the application shuts down. - // - // For the example we use plaintext insecure credentials to avoid needing TLS certificates. To - // use TLS, use TlsChannelCredentials instead. - ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()) + ManagedChannel channel = Grpc.newChannelBuilder(grpcTarget, InsecureChannelCredentials.create()) .build(); try { - HelloWorldClient client = new HelloWorldClient(channel); - client.greet(user); + HelloWorldClient client = new HelloWorldClient(channel, tcpHost, tcpPort); + client.runInteractive(); } finally { - // ManagedChannels use resources like threads and TCP connections. To prevent leaking these - // resources the channel should be shut down when it will no longer be used. If it may be used - // again leave it running. channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } } diff --git a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java index 0e39581c98f..4655634af3f 100644 --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java @@ -1,19 +1,3 @@ -/* - * Copyright 2015 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package io.grpc.examples.helloworld; import io.grpc.Grpc; @@ -26,42 +10,374 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** - * Server that manages startup/shutdown of a {@code Greeter} server. - */ +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; +import java.util.ArrayList; +import java.util.List; + +//tcp imports +import java.net.ServerSocket; +import java.net.Socket; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +//extension imports +import java.io.File; +import java.io.FileWriter; +import java.io.FileReader; +import java.io.BufferedWriter; + + + + + +/* +Current startup: +from examples dir + ./gradlew installDist +./build/install/examples/bin/hello-world-server 50051 50052 backend-1 +./build/install/examples/bin/hello-world-server 50061 50062 backend-2 +./build/install/examples/bin/hello-world-server 50071 50072 backend-3 + +java -cp "build\install\examples\lib\*" io.grpc.examples.helloworld.LoadBalancer 50100 + +./build/install/examples/bin/hello-world-client localhost:50051 + + +*/ + public class HelloWorldServer { private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); private Server server; + + private final int grpcPort; + private final int tcpPort; + private final String serverName; + + static class SharedDataStore { + private static final String MENU_FILE = "restaurant_menu.db"; + private static final String ORDERS_FILE = "restaurant_orders.db"; + private static final String TICKETS_FILE = "restaurant_tickets.db"; + + private static final Object LOCK = new Object(); + + static void ensureFilesExist() { + synchronized (LOCK) { + try { + File menu = new File(MENU_FILE); + File orders = new File(ORDERS_FILE); + File tickets = new File(TICKETS_FILE); + + if (!menu.exists()) menu.createNewFile(); + if (!orders.exists()) orders.createNewFile(); + if (!tickets.exists()) tickets.createNewFile(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize datastore files: " + e.getMessage()); + } + } + } + + static Map loadMenu() { + synchronized (LOCK) { + Map map = new HashMap<>(); + try (BufferedReader br = new BufferedReader(new FileReader(MENU_FILE))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + String[] p = line.split("\\|", -1); + if (p.length < 5) continue; + + String itemId = p[0]; + String name = p[1]; + MenuCategory cat = parseMenuCategory(p[2]); + long cents = parseLongSafe(p[3]); + boolean active = Boolean.parseBoolean(p[4]); + + MenuItem mi = MenuItem.newBuilder() + .setItemId(itemId) + .setName(name) + .setCategory(cat) + .setPrice(Money.newBuilder().setCents(cents).setCurrency("USD").build()) + .setActive(active) + .build(); + + map.put(itemId, mi); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load menu: " + e.getMessage()); + } + return map; + } + } + + static void saveMenu(Map menuItemsById) { + synchronized (LOCK) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(MENU_FILE, false))) { + for (MenuItem mi : menuItemsById.values()) { + bw.write(mi.getItemId() + "|" + + mi.getName() + "|" + + mi.getCategory().name() + "|" + + mi.getPrice().getCents() + "|" + + mi.getActive()); + bw.newLine(); + } + } catch (Exception e) { + throw new RuntimeException("Failed to save menu: " + e.getMessage()); + } + } + } + + static Map loadOrders() { + synchronized (LOCK) { + Map map = new HashMap<>(); + try (BufferedReader br = new BufferedReader(new FileReader(ORDERS_FILE))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + String[] p = line.split("\\|", -1); + if (p.length < 8) continue; + + String orderId = p[0]; + OrderType type = parseOrderType(p[1]); + int tableNumber = parseIntSafe(p[2]); + int guestCount = parseIntSafe(p[3]); + String guestName = p[4]; + OrderStatus status = parseOrderStatus(p[5]); + String ticketId = p[6]; + String lineBlob = p[7]; + + RestaurantCore.OrderRecord o = + new RestaurantCore.OrderRecord(orderId, type, tableNumber, guestCount, guestName); + o.status = status; + o.ticketId = ticketId.isEmpty() ? null : ticketId; + + if (!lineBlob.isEmpty()) { + String[] items = lineBlob.split(","); + for (String item : items) { + String[] f = item.split("\\^", -1); + if (f.length < 5) continue; + + String itemId = f[0]; + String name = f[1]; + int qty = parseIntSafe(f[2]); + long unit = parseLongSafe(f[3]); + long total = parseLongSafe(f[4]); + + OrderLineItem li = OrderLineItem.newBuilder() + .setItemId(itemId) + .setName(name) + .setQuantity(qty) + .setUnitPrice(Money.newBuilder().setCents(unit).setCurrency("USD").build()) + .setLineTotal(Money.newBuilder().setCents(total).setCurrency("USD").build()) + .build(); + + o.lines.add(li); + } + } + + map.put(orderId, o); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load orders: " + e.getMessage()); + } + return map; + } + } + + static void saveOrders(Map orders) { + synchronized (LOCK) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(ORDERS_FILE, false))) { + for (RestaurantCore.OrderRecord o : orders.values()) { + StringBuilder lines = new StringBuilder(); + boolean first = true; + for (OrderLineItem li : o.lines) { + if (!first) lines.append(","); + first = false; + lines.append(li.getItemId()).append("^") + .append(li.getName()).append("^") + .append(li.getQuantity()).append("^") + .append(li.getUnitPrice().getCents()).append("^") + .append(li.getLineTotal().getCents()); + } + + bw.write(o.orderId + "|" + + o.type.name() + "|" + + o.tableNumber + "|" + + o.guestCount + "|" + + nullSafe(o.guestName) + "|" + + o.status.name() + "|" + + nullSafe(o.ticketId) + "|" + + lines); + bw.newLine(); + } + } catch (Exception e) { + throw new RuntimeException("Failed to save orders: " + e.getMessage()); + } + } + } + + static Map loadTickets() { + synchronized (LOCK) { + Map map = new HashMap<>(); + try (BufferedReader br = new BufferedReader(new FileReader(TICKETS_FILE))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + String[] p = line.split("\\|", -1); + if (p.length < 4) continue; + + String ticketId = p[0]; + String orderId = p[1]; + TicketStatus status = parseTicketStatus(p[2]); + String lineBlob = p[3]; + + List lines = new ArrayList<>(); + + if (!lineBlob.isEmpty()) { + String[] items = lineBlob.split(","); + for (String item : items) { + String[] f = item.split("\\^", -1); + if (f.length < 5) continue; + + String itemId = f[0]; + String name = f[1]; + int qty = parseIntSafe(f[2]); + long unit = parseLongSafe(f[3]); + long total = parseLongSafe(f[4]); + + OrderLineItem li = OrderLineItem.newBuilder() + .setItemId(itemId) + .setName(name) + .setQuantity(qty) + .setUnitPrice(Money.newBuilder().setCents(unit).setCurrency("USD").build()) + .setLineTotal(Money.newBuilder().setCents(total).setCurrency("USD").build()) + .build(); + + lines.add(li); + } + } + + RestaurantCore.TicketRecord t = + new RestaurantCore.TicketRecord(ticketId, orderId, lines); + t.status = status; + map.put(ticketId, t); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load tickets: " + e.getMessage()); + } + return map; + } + } + + static void saveTickets(Map tickets) { + synchronized (LOCK) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(TICKETS_FILE, false))) { + for (RestaurantCore.TicketRecord t : tickets.values()) { + StringBuilder lines = new StringBuilder(); + boolean first = true; + for (OrderLineItem li : t.lines) { + if (!first) lines.append(","); + first = false; + lines.append(li.getItemId()).append("^") + .append(li.getName()).append("^") + .append(li.getQuantity()).append("^") + .append(li.getUnitPrice().getCents()).append("^") + .append(li.getLineTotal().getCents()); + } + + bw.write(t.ticketId + "|" + + t.orderId + "|" + + t.status.name() + "|" + + lines); + bw.newLine(); + } + } catch (Exception e) { + throw new RuntimeException("Failed to save tickets: " + e.getMessage()); + } + } + } + + private static String nullSafe(String s) { + return s == null ? "" : s; + } + + private static int parseIntSafe(String s) { + try { return Integer.parseInt(s.trim()); } catch (Exception e) { return 0; } + } + + private static long parseLongSafe(String s) { + try { return Long.parseLong(s.trim()); } catch (Exception e) { return 0L; } + } + + private static MenuCategory parseMenuCategory(String s) { + try { return MenuCategory.valueOf(s); } catch (Exception e) { return MenuCategory.MENU_CATEGORY_UNSPECIFIED; } + } + + private static OrderType parseOrderType(String s) { + try { return OrderType.valueOf(s); } catch (Exception e) { return OrderType.ORDER_TYPE_UNSPECIFIED; } + } + + private static OrderStatus parseOrderStatus(String s) { + try { return OrderStatus.valueOf(s); } catch (Exception e) { return OrderStatus.ORDER_STATUS_UNSPECIFIED; } + } + + private static TicketStatus parseTicketStatus(String s) { + try { return TicketStatus.valueOf(s); } catch (Exception e) { return TicketStatus.TICKET_STATUS_UNSPECIFIED; } + } + } + + + + // Shared core used by BOTH gRPC and TCP + private final RestaurantCore core = new RestaurantCore(); + + public HelloWorldServer(int grpcPort, int tcpPort, String serverName) { + this.grpcPort = grpcPort; + this.tcpPort = tcpPort; + this.serverName = serverName; + } + private void start() throws IOException { - /* The port on which the server should run */ - int port = 50051; - /* - * By default gRPC uses a global, shared Executor.newCachedThreadPool() for gRPC callbacks into - * your application. This is convenient, but can cause an excessive number of threads to be - * created if there are many RPCs. It is often better to limit the number of threads your - * application uses for processing and let RPCs queue when the CPU is saturated. - * The appropriate number of threads varies heavily between applications. - * Async application code generally does not need more threads than CPU cores. - */ ExecutorService executor = Executors.newFixedThreadPool(2); - server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) + + server = Grpc.newServerBuilderForPort(grpcPort, InsecureServerCredentials.create()) .executor(executor) - .addService(new GreeterImpl()) + .addService(new RestaurantImpl(core)) .build() .start(); - logger.info("Server started, listening on " + port); + + logger.info("[" + serverName + "] gRPC Server started, listening on " + grpcPort); + + // Start TCP server in a new thread + Thread tcpThread = new Thread(() -> { + try { + runTcpServer(tcpPort, core, serverName); + } catch (IOException e) { + System.err.println("[" + serverName + "][TCP] Server failed: " + e.getMessage()); + } + }, serverName + "-tcp-server"); + tcpThread.setDaemon(true); + tcpThread.start(); + Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { - // Use stderr here since the logger may have been reset by its JVM shutdown hook. System.err.println("*** shutting down gRPC server since JVM is shutting down"); try { HelloWorldServer.this.stop(); } catch (InterruptedException e) { - if (server != null) { - server.shutdownNow(); - } + if (server != null) server.shutdownNow(); e.printStackTrace(System.err); } finally { executor.shutdown(); @@ -77,31 +393,1010 @@ private void stop() throws InterruptedException { } } - /** - * Await termination on the main thread since the grpc library uses daemon threads. - */ private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } - /** - * Main launches the server from the command line. - */ public static void main(String[] args) throws IOException, InterruptedException { - final HelloWorldServer server = new HelloWorldServer(); + int grpcPort = 50051; + int tcpPort = 50052; + String serverName = "backend-1"; + + if (args.length >= 1) { + grpcPort = parsePort(args[0], 50051); + } + if (args.length >= 2) { + tcpPort = parsePort(args[1], 50052); + } + if (args.length >= 3) { + serverName = args[2]; + } + + final HelloWorldServer server = new HelloWorldServer(grpcPort, tcpPort, serverName); server.start(); server.blockUntilShutdown(); } - static class GreeterImpl extends GreeterGrpc.GreeterImplBase { + private static int parsePort(String s, int fallback) { + try { + int p = Integer.parseInt(s.trim()); + if (p <= 0 || p > 65535) return fallback; + return p; + } catch (Exception e) { + return fallback; + } + } + + // TCP Server + + private static void runTcpServer(int port, RestaurantCore core, String serverName) throws IOException { + try (ServerSocket serverSocket = new ServerSocket(port)) { + System.out.println("[" + serverName + "][TCP] Server listening on port " + port); + + while (true) { + Socket client = serverSocket.accept(); + System.out.println("[" + serverName + "][TCP] Accepted connection from " + client.getRemoteSocketAddress()); + + handleTcpClient(client, core, serverName); + + System.out.println("[" + serverName + "][TCP] Connection closed for " + client.getRemoteSocketAddress()); + } + } + } + + private static void handleTcpClient(Socket client, RestaurantCore core, String serverName) { + try ( + BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true) + ) { + out.println("OK|message=CONNECTED"); + + String line; + while ((line = in.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + if (line.startsWith("QUIT")) { + out.println("OK|message=BYE"); + break; + } + + System.out.println("[" + serverName + "][TCP] Received request: " + line); + + String reply = dispatchTcp(line, core, serverName); + out.println(reply); + + System.out.println("[" +serverName + "][TCP] Sent response: " + reply); + } + } catch (Exception e) { + System.err.println("[" + serverName + "][TCP] Handler error: " + e.getMessage()); + } finally { + try { client.close(); } catch (IOException ignored) {} + } + } + + + private static String dispatchTcp(String requestLine, RestaurantCore core, String serverName) { + String[] parts = requestLine.split("\\|", 2); + String op = parts[0].trim().toUpperCase(); + Map kv = (parts.length > 1) ? parseKv(parts[1]) : new HashMap<>(); + + try { + System.out.println("[" + serverName + "][TCP] Handling op: " + op); + + switch (op) { + case "LOGIN": + return core.tcpLogin(kv.getOrDefault("username",""), kv.getOrDefault("password","")); + case "LOGOUT": + return core.tcpLogout(kv.getOrDefault("token","")); + case "LIST_MENU": + return core.tcpListMenu(kv.getOrDefault("token","")); + case "CREATE_ORDER": + return core.tcpCreateOrder(kv); + case "ADD_ITEM": + return core.tcpAddItem(kv); + case "SUBMIT_ORDER": + return core.tcpSubmitOrder(kv); + case "GET_TICKETS": + return core.tcpGetTickets(kv); + case "ACK_TICKET": + return core.tcpAcknowledgeTicket(kv); + case "GET_BILL": + return core.tcpGetBill(kv); + case "UPDATE_MENU_ITEM": + return core.tcpUpdateMenuItem(kv); + default: + return "ERROR|code=UNKNOWN_OP;message=Unknown op " + op; + } + } catch (Exception e) { + System.out.println("[" + serverName + "][TCP] Exception while handling op " + op + ": " + e.getMessage()); + return "ERROR|code=EXCEPTION;message=" + safe(e.getMessage()); + } +} + + + private static Map parseKv(String s) { + Map map = new HashMap<>(); + String[] pairs = s.split(";"); + for (String pair : pairs) { + pair = pair.trim(); + if (pair.isEmpty()) continue; + int eq = pair.indexOf('='); + if (eq < 0) continue; + String k = pair.substring(0, eq).trim(); + String v = pair.substring(eq + 1).trim(); + map.put(k, v); + } + return map; + } + + private static String safe(String msg) { + if (msg == null) return ""; + return msg.replace("\n", " ").replace("\r", " "); + } + + // Shared Core + + static class RestaurantCore { + + static class UserRecord { + final String username; + final String password; + final UserRole role; + UserRecord(String username, String password, UserRole role) { + this.username = username; + this.password = password; + this.role = role; + } + } + + static class OrderRecord { + final String orderId; + final OrderType type; + final int tableNumber; + final int guestCount; + final String guestName; + OrderStatus status; + final List lines = new ArrayList<>(); + String ticketId; + + OrderRecord(String orderId, OrderType type, int tableNumber, int guestCount, String guestName) { + this.orderId = orderId; + this.type = type; + this.tableNumber = tableNumber; + this.guestCount = guestCount; + this.guestName = guestName; + this.status = OrderStatus.NEW; + } + } + + static class TicketRecord { + final String ticketId; + final String orderId; + TicketStatus status; + final List lines; + TicketRecord(String ticketId, String orderId, List lines) { + this.ticketId = ticketId; + this.orderId = orderId; + this.lines = lines; + this.status = TicketStatus.TICKET_SENT; + } + } + + private final Map users = new HashMap<>(); + private final Map sessions = new HashMap<>(); + private Map menuItemsById = new HashMap<>(); + private Map orders = new HashMap<>(); + private Map tickets = new HashMap<>(); + + RestaurantCore() { + SharedDataStore.ensureFilesExist(); + + users.put("manager", new UserRecord("manager", "pass", UserRole.MANAGER)); + users.put("server", new UserRecord("server", "pass", UserRole.SERVER)); + users.put("chef", new UserRecord("chef", "pass", UserRole.CHEF)); + + menuItemsById = SharedDataStore.loadMenu(); + orders = SharedDataStore.loadOrders(); + tickets = SharedDataStore.loadTickets(); + + + + if (menuItemsById.isEmpty()) + { + putMenuItem("starter_fries", "Fries", MenuCategory.STARTER, 599); + putMenuItem("starter_wings", "Wings", MenuCategory.STARTER, 1099); + putMenuItem("starter_soup", "Soup of the Day", MenuCategory.STARTER, 799); + + putMenuItem("main_burger", "Burger", MenuCategory.MAIN, 1399); + putMenuItem("main_pasta", "Pasta", MenuCategory.MAIN, 1499); + putMenuItem("main_steak", "Steak", MenuCategory.MAIN, 2199); + putMenuItem("main_tacos", "Tacos", MenuCategory.MAIN, 1299); + putMenuItem("main_salmon", "Salmon", MenuCategory.MAIN, 1899); + + putMenuItem("dessert_cake", "Cake", MenuCategory.DESSERT, 699); + putMenuItem("dessert_pie", "Pie", MenuCategory.DESSERT, 649); + putMenuItem("dessert_icecream", "Ice Cream", MenuCategory.DESSERT, 599); + + putMenuItem("drink_coke", "Coke", MenuCategory.DRINK, 299); + putMenuItem("drink_water", "Water", MenuCategory.DRINK, 0); + putMenuItem("drink_tea", "Iced Tea", MenuCategory.DRINK, 249); + putMenuItem("drink_lemonade", "Lemonade", MenuCategory.DRINK, 279); + putMenuItem("drink_coffee", "Coffee", MenuCategory.DRINK, 249); + + SharedDataStore.saveMenu(menuItemsById); + } + } + + private void putMenuItem(String id, String name, MenuCategory cat, long cents) { + MenuItem mi = MenuItem.newBuilder() + .setItemId(id) + .setName(name) + .setCategory(cat) + .setPrice(Money.newBuilder().setCents(cents).setCurrency("USD").build()) + .setActive(true) + .build(); + menuItemsById.put(id, mi); + } + + private boolean isValidSession(String token) { + return token != null && !token.isEmpty() && sessions.containsKey(token); + } + + private UserRole roleFor(String token) { + return sessions.get(token); + } + + private ErrorInfo err(String code, String msg) { + return ErrorInfo.newBuilder().setCode(code).setMessage(msg).build(); + } + + private Money money(long cents) { + return Money.newBuilder().setCents(cents).setCurrency("USD").build(); + } - @Override - public void sayHello(HelloRequest req, StreamObserver responseObserver) { - HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); + private long subtotalCents(OrderRecord o) { + long sum = 0; + for (OrderLineItem li : o.lines) sum += li.getLineTotal().getCents(); + return sum; + } + + private long totalCentsWithTax(long subtotal) { + long tax = (subtotal * 8 + 50) / 100; + return subtotal + tax; + } + + // Core operations + + LoginReply login(LoginRequest req) { + UserRecord u = users.get(req.getUsername()); + if (u == null || !u.password.equals(req.getPassword())) { + return LoginReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("AUTH_FAILED", "Invalid username or password")) + .build(); + } + + String token = UUID.randomUUID().toString(); + sessions.put(token, u.role); + + return LoginReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setSessionToken(token) + .setRole(u.role) + .build(); + } + + StatusReply logout(LogoutRequest req) { + if (!isValidSession(req.getSessionToken())) { + return StatusReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Invalid or expired session token")) + .build(); + } + sessions.remove(req.getSessionToken()); + return StatusReply.newBuilder().setStatus(ReplyStatus.OK).build(); } + + UpdateMenuItemReply updateMenuItem(UpdateMenuItemRequest req) { + if (!isValidSession(req.getSessionToken())) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.MANAGER) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only MANAGER can edit the menu")) + .build(); + } + + menuItemsById = SharedDataStore.loadMenu(); + + String id = req.getItemId(); + if (id == null || id.trim().isEmpty()) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "item_id required")) + .build(); + } + + MenuItem existing = menuItemsById.get(id); + + String name = (existing != null) ? existing.getName() : ""; + MenuCategory cat = (existing != null) ? existing.getCategory() : MenuCategory.MENU_CATEGORY_UNSPECIFIED; + long cents = (existing != null) ? existing.getPrice().getCents() : 0; + boolean active = (existing != null) ? existing.getActive() : true; + + if (req.getName() != null && !req.getName().trim().isEmpty()) name = req.getName().trim(); + if (req.getCategory() != MenuCategory.MENU_CATEGORY_UNSPECIFIED) cat = req.getCategory(); + if (req.getPriceCents() >= 0) cents = req.getPriceCents(); + if (req.getHasActive()) active = req.getActive(); + + if (name.isEmpty()) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "name required (for new items)")) + .build(); + } + if (cat == MenuCategory.MENU_CATEGORY_UNSPECIFIED) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "category required (for new items)")) + .build(); + } + if (cents < 0) { + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "price_cents must be >= 0")) + .build(); + } + + MenuItem updated = MenuItem.newBuilder() + .setItemId(id) + .setName(name) + .setCategory(cat) + .setPrice(Money.newBuilder().setCents(cents).setCurrency("USD").build()) + .setActive(active) + .build(); + + menuItemsById.put(id, updated); + SharedDataStore.saveMenu(menuItemsById); + + return UpdateMenuItemReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setItem(updated) + .build(); + } + + ListMenuReply listMenu(ListMenuRequest req) { + if (!isValidSession(req.getSessionToken())) { + return ListMenuReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + menuItemsById = SharedDataStore.loadMenu(); + Menu.Builder mb = Menu.newBuilder(); + for (MenuItem mi : menuItemsById.values()) { + if (mi.getActive()) mb.addItems(mi); + } + + return ListMenuReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setMenu(mb.build()) + .build(); + } + + CreateOrderReply createOrder(CreateOrderRequest req) { + if (!isValidSession(req.getSessionToken())) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + + if (roleFor(req.getSessionToken()) != UserRole.SERVER) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only SERVER role can create orders")) + .build(); + } + orders = SharedDataStore.loadOrders(); + + if (req.getType() == OrderType.DINE_IN) { + int table = req.getTableNumber(); + if (table < 1 || table > 50) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "Only table_number=1 is supported (single-table restaurant)")) + .build(); + } + if (req.getGuestCount() <= 0 || req.getGuestCount() > 20) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "guest_count must be 1..20")) + .build(); + } + } else if (req.getType() == OrderType.TAKEOUT) { + if (req.getGuestName() == null || req.getGuestName().isEmpty()) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "Takeout requires guest_name")) + .build(); + } + if (hasActiveTakeoutOrder()) { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("LIMIT", "Only one active TAKEOUT order is allowed at a time")) + .build(); + } + } else { + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "Order type required")) + .build(); + } + + String orderId = "order-" + UUID.randomUUID(); + OrderRecord o = new OrderRecord( + orderId, + req.getType(), + req.getTableNumber(), + req.getGuestCount(), + req.getGuestName() + ); + orders.put(orderId, o); + + System.out.println("[CORE] Created order " + orderId + " type=" + req.getType().name()); + + SharedDataStore.saveOrders(orders); + + return CreateOrderReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setOrderId(orderId) + .setOrderStatus(o.status) + .build(); + } + + AddItemReply addItem(AddItemRequest req) { + if (!isValidSession(req.getSessionToken())) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.SERVER) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only SERVER role can add items")) + .build(); + } + + orders = SharedDataStore.loadOrders(); + menuItemsById = SharedDataStore.loadMenu(); + + OrderRecord o = orders.get(req.getOrderId()); + if (o == null) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_FOUND", "Order not found")) + .build(); + } + if (o.status != OrderStatus.NEW) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID_STATE", "Can only add items while order is NEW")) + .build(); + } + + if (req.getQuantity() <= 0) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "quantity must be > 0")) + .build(); + } + + MenuItem mi = menuItemsById.get(req.getItemId()); + if (mi == null || !mi.getActive()) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_FOUND", "Menu item not found")) + .build(); + } + + if (o.type == OrderType.TAKEOUT) { + int current = totalItemCount(o); + int next = current + req.getQuantity(); + if (next > 10) { + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("LIMIT", "Takeout orders are limited to 10 total items")) + .build(); + } + } + + long unit = mi.getPrice().getCents(); + long lineTotal = unit * (long) req.getQuantity(); + + OrderLineItem li = OrderLineItem.newBuilder() + .setItemId(mi.getItemId()) + .setName(mi.getName()) + .setQuantity(req.getQuantity()) + .setUnitPrice(money(unit)) + .setLineTotal(money(lineTotal)) + .build(); + + o.lines.add(li); + + SharedDataStore.saveOrders(orders); + + return AddItemReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setOrderId(o.orderId) + .setLineCount(o.lines.size()) + .build(); + } + + SubmitOrderReply submitOrder(SubmitOrderRequest req) { + if (!isValidSession(req.getSessionToken())) { + return SubmitOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.SERVER) { + return SubmitOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only SERVER role can submit orders")) + .build(); + } + + orders = SharedDataStore.loadOrders(); + tickets = SharedDataStore.loadTickets(); + + OrderRecord o = orders.get(req.getOrderId()); + if (o == null) { + return SubmitOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_FOUND", "Order not found")) + .build(); + } + if (o.lines.isEmpty()) { + return SubmitOrderReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("INVALID", "Cannot submit empty order")) + .build(); + } + + String ticketId = "ticket-" + UUID.randomUUID(); + o.ticketId = ticketId; + o.status = OrderStatus.SENT_TO_KITCHEN; + + List copied = new ArrayList<>(o.lines); + TicketRecord t = new TicketRecord(ticketId, o.orderId, copied); + tickets.put(ticketId, t); + + SharedDataStore.saveOrders(orders); + SharedDataStore.saveTickets(tickets); + + long sub = subtotalCents(o); + long tot = totalCentsWithTax(sub); + + return SubmitOrderReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setOrderId(o.orderId) + .setOrderStatus(o.status) + .setTicketId(ticketId) + .setSubtotal(money(sub)) + .setTotal(money(tot)) + .build(); + } + + GetTicketsReply getTickets(GetTicketsRequest req) { + if (!isValidSession(req.getSessionToken())) { + return GetTicketsReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.CHEF) { + return GetTicketsReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only CHEF role can get tickets")) + .build(); + } + + tickets = SharedDataStore.loadTickets(); + TicketStatus filter = req.getStatusFilter(); + GetTicketsReply.Builder rb = GetTicketsReply.newBuilder().setStatus(ReplyStatus.OK); + + for (TicketRecord t : tickets.values()) { + if (filter != null && filter != TicketStatus.TICKET_STATUS_UNSPECIFIED) { + if (t.status != filter) continue; + } + KitchenTicket.Builder kb = KitchenTicket.newBuilder() + .setTicketId(t.ticketId) + .setOrderId(t.orderId) + .setStatus(t.status); + for (OrderLineItem li : t.lines) kb.addLines(li); + rb.addTickets(kb.build()); + } + + return rb.build(); + } + + AcknowledgeTicketReply acknowledgeTicket(AcknowledgeTicketRequest req) { + if (!isValidSession(req.getSessionToken())) { + return AcknowledgeTicketReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.CHEF) { + return AcknowledgeTicketReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only CHEF role can acknowledge tickets")) + .build(); + } + + tickets = SharedDataStore.loadTickets(); + orders = SharedDataStore.loadOrders(); + + TicketRecord t = tickets.get(req.getTicketId()); + if (t == null) { + return AcknowledgeTicketReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_FOUND", "Ticket not found")) + .build(); + } + + t.status = TicketStatus.TICKET_ACKNOWLEDGED; + + + OrderRecord o = orders.get(t.orderId); + if (o != null) o.status = OrderStatus.ACKNOWLEDGED; + + SharedDataStore.saveTickets(tickets); + SharedDataStore.saveOrders(orders); + + return AcknowledgeTicketReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setTicketId(t.ticketId) + .setTicketStatus(t.status) + .setOrderId(t.orderId) + .setOrderStatus(o != null ? o.status : OrderStatus.ORDER_STATUS_UNSPECIFIED) + .build(); + } + + GetBillReply getBill(GetBillRequest req) { + if (!isValidSession(req.getSessionToken())) { + return GetBillReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_LOGGED_IN", "Login required")) + .build(); + } + if (roleFor(req.getSessionToken()) != UserRole.SERVER) { + return GetBillReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("FORBIDDEN", "Only SERVER role can request bill")) + .build(); + } + + orders = SharedDataStore.loadOrders(); + + OrderRecord o = orders.get(req.getOrderId()); + if (o == null) { + return GetBillReply.newBuilder() + .setStatus(ReplyStatus.ERROR) + .setError(err("NOT_FOUND", "Order not found")) + .build(); + } + + + long sub = subtotalCents(o); + long tot = totalCentsWithTax(sub); + + Bill.Builder bb = Bill.newBuilder() + .setOrderId(o.orderId) + .setSubtotal(money(sub)) + .setTotal(money(tot)); + + for (OrderLineItem li : o.lines) bb.addLines(li); + + return GetBillReply.newBuilder() + .setStatus(ReplyStatus.OK) + .setBill(bb.build()) + .build(); + } + + // helper funcs + + private int totalItemCount(OrderRecord o) { + int sum = 0; + for (OrderLineItem li : o.lines) sum += li.getQuantity(); + return sum; + } + + private boolean isOrderActive(OrderRecord o) { + return o.status == OrderStatus.NEW + || o.status == OrderStatus.SENT_TO_KITCHEN + || o.status == OrderStatus.ACKNOWLEDGED; + } + + private boolean hasActiveTakeoutOrder() { + for (OrderRecord o : orders.values()) { + if (o.type == OrderType.TAKEOUT && isOrderActive(o)) return true; + } + return false; + } + + private static String moneyToString(Money m) { + long cents = m.getCents(); + long dollars = cents / 100; + long rem = Math.abs(cents % 100); + return m.getCurrency() + " " + dollars + "." + (rem < 10 ? "0" + rem : "" + rem); + } + + // TCP wrappers + + String tcpLogin(String username, String password) { + LoginReply r = login(LoginRequest.newBuilder().setUsername(username).setPassword(password).build()); + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + return "OK|token=" + r.getSessionToken() + ";role=" + r.getRole().name(); + } + + String tcpLogout(String token) { + StatusReply r = logout(LogoutRequest.newBuilder().setSessionToken(token).build()); + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + return "OK|status=OK"; + } + + String tcpUpdateMenuItem(Map kv) { + String token = kv.getOrDefault("token", ""); + String itemId = kv.getOrDefault("itemId", ""); + + UpdateMenuItemRequest.Builder b = UpdateMenuItemRequest.newBuilder() + .setSessionToken(token) + .setItemId(itemId); + + String name = kv.getOrDefault("name", "").trim(); + if (!name.isEmpty()) b.setName(name); + + String catStr = kv.getOrDefault("category", "").trim().toUpperCase(); + if (!catStr.isEmpty()) { + MenuCategory cat = MenuCategory.MENU_CATEGORY_UNSPECIFIED; + if ("STARTER".equals(catStr)) cat = MenuCategory.STARTER; + if ("MAIN".equals(catStr)) cat = MenuCategory.MAIN; + if ("DESSERT".equals(catStr)) cat = MenuCategory.DESSERT; + if ("DRINK".equals(catStr)) cat = MenuCategory.DRINK; + b.setCategory(cat); + } + + String priceStr = kv.getOrDefault("price", "").trim(); + if (!priceStr.isEmpty()) { + long cents; + try { cents = Long.parseLong(priceStr); } catch (Exception e) { cents = -2; } + b.setPriceCents(cents); + } else { + b.setPriceCents(-1); + } + + String activeStr = kv.getOrDefault("active", "").trim().toLowerCase(); + if (!activeStr.isEmpty()) { + b.setHasActive(true); + b.setActive("true".equals(activeStr) || "1".equals(activeStr) || "yes".equals(activeStr)); + } else { + b.setHasActive(false); + } + + UpdateMenuItemReply r = updateMenuItem(b.build()); + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + + MenuItem mi = r.getItem(); + return "OK|itemId=" + mi.getItemId() + + ";name=" + safe(mi.getName()) + + ";category=" + mi.getCategory().name() + + ";price=" + mi.getPrice().getCents() + + ";active=" + mi.getActive(); + } + + String tcpListMenu(String token) { + ListMenuReply r = listMenu(ListMenuRequest.newBuilder().setSessionToken(token).build()); + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + + StringBuilder sb = new StringBuilder(); + sb.append("OK|items="); + + boolean first = true; + for (MenuItem mi : r.getMenu().getItemsList()) { + if (!first) sb.append(","); + first = false; + sb.append(mi.getItemId()).append("^") + .append(mi.getName()).append("^") + .append(mi.getCategory().name()).append("^") + .append(mi.getPrice().getCents()); + } + return sb.toString(); + } + + String tcpCreateOrder(Map kv) { + String token = kv.getOrDefault("token", ""); + String typeStr = kv.getOrDefault("type", "DINE_IN").toUpperCase(); + + CreateOrderRequest.Builder b = CreateOrderRequest.newBuilder().setSessionToken(token); + + if ("DINE_IN".equals(typeStr)) { + b.setType(OrderType.DINE_IN); + int table = parseIntSafe(kv.getOrDefault("table", "0")); + int guests = parseIntSafe(kv.getOrDefault("guests", "0")); + b.setTableNumber(table).setGuestCount(guests); + } else if ("TAKEOUT".equals(typeStr)) { + b.setType(OrderType.TAKEOUT); + b.setGuestName(kv.getOrDefault("guest", "")); + } else { + b.setType(OrderType.ORDER_TYPE_UNSPECIFIED); + } + + CreateOrderReply r = createOrder(b.build()); + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + return "OK|orderId=" + r.getOrderId() + ";orderStatus=" + r.getOrderStatus().name(); + } + + String tcpAddItem(Map kv) { + String token = kv.getOrDefault("token", ""); + String orderId = kv.getOrDefault("orderId", ""); + String itemId = kv.getOrDefault("itemId", ""); + int qty = parseIntSafe(kv.getOrDefault("qty", "0")); + + AddItemReply r = addItem(AddItemRequest.newBuilder() + .setSessionToken(token) + .setOrderId(orderId) + .setItemId(itemId) + .setQuantity(qty) + .build()); + + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + return "OK|orderId=" + r.getOrderId() + ";lineCount=" + r.getLineCount(); + } + + String tcpSubmitOrder(Map kv) { + String token = kv.getOrDefault("token", ""); + String orderId = kv.getOrDefault("orderId", ""); + + SubmitOrderReply r = submitOrder(SubmitOrderRequest.newBuilder() + .setSessionToken(token) + .setOrderId(orderId) + .build()); + + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + return "OK|orderId=" + r.getOrderId() + + ";orderStatus=" + r.getOrderStatus().name() + + ";ticketId=" + r.getTicketId() + + ";subtotal=" + safe(moneyToString(r.getSubtotal())) + + ";total=" + safe(moneyToString(r.getTotal())); + } + + String tcpGetTickets(Map kv) { + String token = kv.getOrDefault("token", ""); + String statusStr = kv.getOrDefault("status", "TICKET_SENT").toUpperCase(); + + TicketStatus filter = TicketStatus.TICKET_STATUS_UNSPECIFIED; + if ("TICKET_SENT".equals(statusStr)) filter = TicketStatus.TICKET_SENT; + if ("TICKET_ACKNOWLEDGED".equals(statusStr)) filter = TicketStatus.TICKET_ACKNOWLEDGED; + + GetTicketsReply r = getTickets(GetTicketsRequest.newBuilder() + .setSessionToken(token) + .setStatusFilter(filter) + .build()); + + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + + StringBuilder sb = new StringBuilder(); + sb.append("OK|count=").append(r.getTicketsCount()).append(";tickets="); + + boolean first = true; + for (KitchenTicket t : r.getTicketsList()) { + if (!first) sb.append(","); + first = false; + sb.append(t.getTicketId()).append("^") + .append(t.getOrderId()).append("^") + .append(t.getStatus().name()); + } + return sb.toString(); + } + + String tcpAcknowledgeTicket(Map kv) { + String token = kv.getOrDefault("token", ""); + String ticketId = kv.getOrDefault("ticketId", ""); + + AcknowledgeTicketReply r = acknowledgeTicket(AcknowledgeTicketRequest.newBuilder() + .setSessionToken(token) + .setTicketId(ticketId) + .build()); + + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + + return "OK|ticketId=" + r.getTicketId() + + ";ticketStatus=" + r.getTicketStatus().name() + + ";orderId=" + r.getOrderId() + + ";orderStatus=" + r.getOrderStatus().name(); + } + + String tcpGetBill(Map kv) { + String token = kv.getOrDefault("token", ""); + String orderId = kv.getOrDefault("orderId", ""); + + GetBillReply r = getBill(GetBillRequest.newBuilder() + .setSessionToken(token) + .setOrderId(orderId) + .build()); + + if (r.getStatus() != ReplyStatus.OK) { + return "ERROR|code=" + r.getError().getCode() + ";message=" + safe(r.getError().getMessage()); + } + + Bill b = r.getBill(); + StringBuilder sb = new StringBuilder(); + sb.append("OK|orderId=").append(b.getOrderId()) + .append(";subtotal=").append(safe(moneyToString(b.getSubtotal()))) + .append(";total=").append(safe(moneyToString(b.getTotal()))) + .append(";lines="); + + boolean first = true; + for (OrderLineItem li : b.getLinesList()) { + if (!first) sb.append(","); + first = false; + sb.append(li.getItemId()).append("^") + .append(li.getName()).append("^") + .append(li.getQuantity()).append("^") + .append(li.getLineTotal().getCents()); + } + return sb.toString(); + } + + private static int parseIntSafe(String s) { + try { return Integer.parseInt(s.trim()); } catch (Exception ignored) { return 0; } + } + } + + // gRPC service wrapper + + static class RestaurantImpl extends RestaurantGrpc.RestaurantImplBase { + private final RestaurantCore core; + + RestaurantImpl(RestaurantCore core) { this.core = core; } + + @Override public void login(LoginRequest req, StreamObserver obs) { obs.onNext(core.login(req)); obs.onCompleted(); } + @Override public void logout(LogoutRequest req, StreamObserver obs) { obs.onNext(core.logout(req)); obs.onCompleted(); } + @Override public void listMenu(ListMenuRequest req, StreamObserver obs) { obs.onNext(core.listMenu(req)); obs.onCompleted(); } + @Override public void createOrder(CreateOrderRequest req, StreamObserver obs) { obs.onNext(core.createOrder(req)); obs.onCompleted(); } + @Override public void addItem(AddItemRequest req, StreamObserver obs) { obs.onNext(core.addItem(req)); obs.onCompleted(); } + @Override public void submitOrder(SubmitOrderRequest req, StreamObserver obs) { obs.onNext(core.submitOrder(req)); obs.onCompleted(); } + @Override public void getTickets(GetTicketsRequest req, StreamObserver obs) { obs.onNext(core.getTickets(req)); obs.onCompleted(); } + @Override public void acknowledgeTicket(AcknowledgeTicketRequest req, StreamObserver obs) { obs.onNext(core.acknowledgeTicket(req)); obs.onCompleted(); } + @Override public void getBill(GetBillRequest req, StreamObserver obs) { obs.onNext(core.getBill(req)); obs.onCompleted(); } + @Override public void updateMenuItem(UpdateMenuItemRequest req, StreamObserver obs) { obs.onNext(core.updateMenuItem(req)); obs.onCompleted(); } } } diff --git a/examples/src/main/java/io/grpc/examples/helloworld/LoadBalanacer.java b/examples/src/main/java/io/grpc/examples/helloworld/LoadBalanacer.java new file mode 100644 index 00000000000..a92f1498075 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/helloworld/LoadBalanacer.java @@ -0,0 +1,195 @@ +package io.grpc.examples.helloworld; + +import java.net.ServerSocket; +import java.net.Socket; +import java.net.InetSocketAddress; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +public class LoadBalancer { + + static class BackendServer { + final String name; + final String host; + final int port; + int activeConnections; + + BackendServer(String name, String host, int port) { + this.name = name; + this.host = host; + this.port = port; + this.activeConnections = 0; + } + } + + private final int listenPort; + private final List backends; + + public LoadBalancer(int listenPort, List backends) { + this.listenPort = listenPort; + this.backends = Collections.synchronizedList(backends); + } + + public void start() throws IOException { + try (ServerSocket serverSocket = new ServerSocket(listenPort)) { + System.out.println("[LB] Listening on port " + listenPort); + + while (true) { + Socket clientSocket = serverSocket.accept(); + BackendServer backend = chooseLeastConnectionsBackend(); + + if (backend == null) { + try (PrintWriter out = new PrintWriter( + new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true)) { + out.println("ERROR|code=NO_BACKEND;message=No backend server available"); + } catch (Exception ignored) { + } finally { + try { clientSocket.close(); } catch (Exception ignored) {} + } + continue; + } + + incrementConnections(backend); + + Thread t = new Thread(() -> { + try { + handleClient(clientSocket, backend); + } finally { + decrementConnections(backend); + } + }, "lb-client-" + clientSocket.getPort()); + + t.start(); + } + } + } + + private BackendServer chooseLeastConnectionsBackend() { + synchronized (backends) { + BackendServer best = null; + for (BackendServer b : backends) { + if (best == null || b.activeConnections < best.activeConnections) { + best = b; + } + } + return best; + } + } + + private void incrementConnections(BackendServer backend) { + synchronized (backends) { + backend.activeConnections++; + System.out.println("[LB] Routed new client to " + backend.name + + " (" + backend.host + ":" + backend.port + ")" + + " active=" + backend.activeConnections); + } + } + + private void decrementConnections(BackendServer backend) { + synchronized (backends) { + if (backend.activeConnections > 0) { + backend.activeConnections--; + } + System.out.println("[LB] Client disconnected from " + backend.name + + " active=" + backend.activeConnections); + } + } + + private void handleClient(Socket clientSocket, BackendServer backend) { + Socket backendSocket = null; + + try ( + BufferedReader clientIn = new BufferedReader( + new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8)); + PrintWriter clientOut = new PrintWriter( + new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true) + ) { + backendSocket = new Socket(); + backendSocket.connect(new InetSocketAddress(backend.host, backend.port), 3000); + + try ( + BufferedReader backendIn = new BufferedReader( + new InputStreamReader(backendSocket.getInputStream(), StandardCharsets.UTF_8)); + PrintWriter backendOut = new PrintWriter( + new OutputStreamWriter(backendSocket.getOutputStream(), StandardCharsets.UTF_8), true) + ) { + // Forward backend greeting first + String greeting = backendIn.readLine(); + if (greeting != null) { + clientOut.println(greeting); + } else { + clientOut.println("ERROR|code=BACKEND_DOWN;message=Backend did not respond"); + return; + } + + String line; + while ((line = clientIn.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + System.out.println("[LB] client=" + clientSocket.getRemoteSocketAddress() + + " -> " + backend.name + " request=" + line); + + backendOut.println(line); + + String response = backendIn.readLine(); + if (response == null) { + clientOut.println("ERROR|code=BACKEND_DOWN;message=Backend connection lost"); + break; + } + + clientOut.println(response); + + if (line.startsWith("QUIT")) { + break; + } + } + } + } catch (Exception e) { + System.out.println("[LB] Handler error: " + e.getMessage()); + try { + PrintWriter clientOut = new PrintWriter( + new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true); + clientOut.println("ERROR|code=LB_EXCEPTION;message=" + safe(e.getMessage())); + } catch (Exception ignored) { + } + } finally { + try { clientSocket.close(); } catch (Exception ignored) {} + try { + if (backendSocket != null) backendSocket.close(); + } catch (Exception ignored) {} + } + } + + private static String safe(String msg) { + if (msg == null) return ""; + return msg.replace("\n", " ").replace("\r", " "); + } + + public static void main(String[] args) throws Exception { + int listenPort = 50100; + if (args.length >= 1) { + try { + listenPort = Integer.parseInt(args[0]); + } catch (Exception ignored) { + listenPort = 50100; + } + } + + List backends = new ArrayList<>(); + backends.add(new BackendServer("backend-1", "localhost", 50052)); + backends.add(new BackendServer("backend-2", "localhost", 50062)); + backends.add(new BackendServer("backend-3", "localhost", 50072)); + + LoadBalancer lb = new LoadBalancer(listenPort, backends); + lb.start(); + } +}