Memra

Worked build: the Poem-of-the-Day server

◈ 7 cards

Implement the specified FSM over ServerSocket — poems loaded once at startup, one state variable and one switch, CRLF framing with a flush before every read — then accept-test it with telnet, one session per table row.

1. Load the poems once, at startup

The assignment fixes the shape: java PodServer poems.txt. Read args[0] before the server socket is opened, parse the file into a List<Poem> once, and hold it for the lifetime of the process. Re-reading per connection is the classic mistake — it turns a memory lookup into a disk read on every request, and it means a file edited mid-run silently changes what different clients see.

Fail loudly. No argument: print a usage line and exit non-zero. File missing, unreadable, or parsing to zero poems: say which and exit. A server that starts happily and then answers every client with 'no poems available' is worse than one that refuses to start, because the failure surfaces at the marker rather than at you.

2. Accept, then hand off

The outer loop is module 7's, unchanged: bind, accept() forever, submit each returned Socket to a fixed thread pool. The entire per-connection try/catch lives inside the handler, so one client's IOException can never end the accept loop. PoD sessions are short — greet, one line in, one poem out, close — so a small pool is ample.

3. One state variable, one switch

This is the step that makes the marker's job, and yours, easy. The handler holds exactly one field of the state enum and loops until it reaches the terminal state. Every case is one row of your transition table, in the same order, using the same names. When the table says LISTED × out-of-range → write 400, go to REJECTED, the code says it too, and the two can be read side by side and checked line by line.

The alternative — a chain of nested ifs with the state implied by how deep you have nested — is functionally equivalent and impossible to audit. You cannot point at it and say 'this is row four'.

4. Framing: CRLF out, flush before every read

Write \r\n explicitly. Never println, which emits the platform separator and swallows IOExceptionPrintWriter only reports failure through checkError(). Read with BufferedReader.readLine(), which strips the terminator, so the string you get back has no CRLF and you must put one back on every line you send.

And flush before every read. A buffered writer holds your prompt in memory; if you then block in readLine() waiting for a choice the client has not been asked for yet, both sides wait forever. That is the hang framing bugs produce, and it is the single commonest reason a PoD server that 'works' fails its telnet test.

5. The error transition, then the close

One handler covers every input the grammar forbids: non-numeric, empty, out of range, end-of-stream. Each writes its specified line and moves to REJECTED — never back to LISTED, because the specification says the session ends after one choice, right or wrong. Then close in a finally, or put the Socket in a try-with-resources, so the socket is released down every path including the exceptional ones.

Worked example — the telnet acceptance script

Your test plan is the transition table, one telnet session per row. Start the server, then telnet localhost 7000:

  1. Row 2 — type 2. Expect the second poem, then Connection closed by foreign host.
  2. Row 3 — reconnect, type 99. Expect your out-of-range line, then a close.
  3. Row 4 — reconnect, type banana. Expect your bad-request line, then a close.
  4. Row 5 — reconnect and disconnect without typing anything. Expect the server to log the drop and release the thread, not to spin.

Every row exercised, including the ones you did not want to write. That transcript, pasted into your documentation, is the test plan the assignment asks for.

acceptvalidbaddonedoneCONNECTEDgreet()LISTEDreadChoice()SENTsendPoem()REJECTEDsendError()CLOSEDclose()One method per state, one case per row.
The same five states as the specification, now annotated with the method that implements each one. If a method name does not appear next to a state, that row of the table has no code behind it.
t1t2t3t4clienttelnet :70002servergreeting + listpoem + closeOne turn each way, then the server closes.
The happy-path acceptance test, turn by turn. Note that nothing appears in the client row at t4 — the server closes, which is what makes telnet print "Connection closed by foreign host".
NORMAL ~/memra/learn/comp-348/poem-of-the-day-server utf-8 LF