Homework 7: PennPals

Computing’s core challenge is how not to make a mess of it.
     — Edsger W. Dijkstra


Assignment Overview

In this assignment, you will be using Java to build an internet chat server. In particular, your task will be to design and model the internal state of the server and handle communication with users of the chat system.

HW07 does not have tasks. Instead, it has checkpoints written directly into its description and table of contents (on the left of this page).

How to Approach this Project

This project, like the OCaml Paint project, is designed to get you comfortable working with larger software projects with a lot of moving parts.

This project will be significantly easier if you approach it methodically and follow the program design process!

When you encounter errors or confusion, you may feel the need to refer back to the instructions and specifications. This is perfectly normal—we don’t expect you to memorize every tiny detail of the server specification at once! Programming with documentation can be overwhelming at times, but it is good practice for larger software engineering work. Understand the big picture first; work through the details of implementation at each part by referring back to the instructions. Use the instructions to clarify any confusion or edge cases you encounter.

Pay careful attention to your coding style as you complete this assignment. Things like modularity of design will come naturally if you follow the layout of the tasks listed below and plan ahead. Other stylistic concerns, such as naming conventions and formatting, will require more of an ongoing effort. This assignment has a lot of interrelated components, and you will probably find it rewarding to maintain a fair amount of stylistic discipline when it comes to field and method naming. A poorly named data structure, when it is one of many, can quickly add significant cognitive overhead to tracing through your code, should you need to debug it!

Setting Up

The Codio project should be set up with all the requisite files.

For Eclipse, you’ll need to follow the Eclipse setup instructions for Making a Java Project and import the stub files.

In both cases, make sure to keep the PLAN.txt file handy - you should use it to write notes about your design ideas as you learn more about the assignment and decide how you’d like to design your program.


Task 1: Understanding the Problem

Client-Server Architecture

A client-server architecture is a pattern for building a distributed system that requires the coordination of a (potentially large) number of different computers. Email and the World Wide Web are examples of such systems; they allow multiple clients, usually the end users of the service, to exchange data with servers. A server is a computer system designed to provide a service to many connected clients at once, often facilitating communication between them.

Clients communicate with the server by means of a protocol, which is a standardized set of instructions for requesting and transmitting information over the internet. For example, you might have heard of HTTP (HyperText Transfer Protocol), which allows browsers to request web pages from a server. Our server and clients will use the PennPals protocol (explained in greater detail below) to communicate with each other.

User IDs and Nicknames

The ServerBackend class is the base layer of the server, which handles network connections with various clients. When a client connects to the server for the first time, the ServerBackend generates a unique integer ID to represent the open connection with the client. (There are no guarantees about these integers’ values beyond uniqueness.) Because chatting with other clients based on their ID number is confusing, we will allow clients to have nicknames which are used within the application. Unlike user IDs, which cannot change as long as the client is connected to the server, clients can change their nicknames at any time.

Channels and Owners

The server does not serve as one giant chat room for all connected users. Instead, this protocol is modeled around the idea of channels, or groupings of users on the server. The server has multiple channels, and a client may be in any number of channels (or in none). Any user who opts to join the discussion in a channel will receive all messages and commands that are directed to that channel after they join. At any point a user can also leave a channel, and will then stop receiving messages and commands.

The user that created a channel is designated as that channel’s owner. The owner of a channel may kick other users from the channel. If the channel is invite-only, the owner of the channel must add other users by sending an InviteCommand. For the sake of simplicity, the owner of a channel cannot be changed; a channel is removed if its owner leaves.

Checkpoint 1

Make sure you understand the definitions of the following terms in the context of this homework assignment: client, server, protocol, channel, nickname, and owner.

Class Diagram

To guide you along this project, we’ve provided a diagram to explain how the parts work together:

PennPals Chat Protocol

The PennPals protocol is a set of commands that can be sent to the server from a client, who is known as the sender of the command. Some commands also include a target user, who is the object of an action that the sender wishes the server to perform.

The table below summarizes each of the client commands:

CommandEffect
NicknameCommandChanges the sender’s nickname.
CreateCommandCreates a new channel with the sender as its owner.
JoinCommandAdds the sender to a public channel.
InviteCommandInvites the target user to a private channel owned by the sender.
MessageCommandDelivers a message to all users in a channel.
LeaveCommandRemoves the sender from a channel.
KickCommandRemoves the target user from a private channel.

How Commands are Represented

Each of the commands of the PennPals protocol above are represented by a class of the same name. These classes all extend a common abstract superclass, called Command. Because Command is abstract, it cannot be directly instantiated. However, as an abstract class it provides a common interface for all commands and a common implementation for some of the features that all commands should support, such as the name of the sender of the command.

How Commands are Communicated Between the Server and the Clients

The server and client communicate using a text-based protocol, where commands are encoded as strings and transmitted as a stream of characters. When the server receives a command string from a client, it must use the CommandParser class to convert the string into a corresponding Command object.

For example, consider the following command string:

:camel MESG java :CIS 120 is the best!

This string is a command issued by the client whose nickname is camel, instructing the server to deliver the message CIS 120 is the best! to every user in the java channel.

For this project, we have provided you with the code necessary to convert Command objects to and from strings. In particular, each subclass of Command overrides the toString method to produce strings (in the client) and the server uses the CommandParser for the opposite conversion. You don’t need to be concerned with the specifics of this process, but you should take a look at the various toString implementations because you will likely see commands printed to the console when you run your server.

Subtyping and Dynamic Dispatch

When a server receives a command from a client, it must process that command in some way. Therefore, every concrete subclass of Command must implement the following abstract method:

public abstract Broadcast updateServerModel(ServerModel model)

Because this method is an abstract method of the Command class, the server backend can call updateServerModel with any command and rely on dynamic dispatch to execute the appropriate behavior.

Your job will be to implement the updateServerModel method for each concrete subclass of Command, allowing the server to process commands sent by its clients. Before that, however, you will need to think about how the server state should be modeled.

Testing

An important part of any large software project is writing test cases. Tests help you define expected behavior before diving too deep into the implementation. Throughout this homework, make sure to write tests as you go. Writing tests before attempting to implement some functionality will help you get a better understanding of the task and help you verify that the code you’ve written works as expected.

Testing with the GUI client is not sufficient to ensure that your code works as expected. Make sure to write JUnit tests in addition to any manual testing you perform. Use the tests in ServerModelTest.java as a guide.


Task 2: Designing the ServerModel

The ServerModel is the component responsible for processing each Command that is generated by the parser, and for keeping track of the server state. Your model will need to keep track of all the users connected to the server (see task 3) and the channels they are in (task 4 and task 5).

Before continuing, read the ServerModel documentation to see what methods the ServerModel class must provide. The structure of the server’s state should make it convenient to implement those methods!

Selecting the appropriate data structures to model your application state is one of the most important design considerations in software engineering. An improper choice can lead to numerous headaches down the road, and can be difficult to change later on because the model’s implementation relies so heavily on the data structures it uses.

For this reason, we encourage you to take some time to think through the requirements for the server model and plan your choice of collections before beginning your implementation. Grab some coffee, go for a walk, listen to some music—but don’t write any code until you’ve put some thought into your design!

Skim through the remainder of these instructions to get a sense of what is expected from the ServerModel class. Decide what information the model will be responsible for storing, and how it should be grouped and associated. There isn’t one right answer, but certain implementations are preferable because they limit the complexity associated with manipulating and using the data.

Try to eliminate redundancy and cyclic references in the data you store. Don’t store the same data in two different places, or it might get out of sync!

The next part of the instructions for this task will provide general advice about model design. You should take it into account—a significant portion of your grade for this assignment is based on the clarity and efficiency of the models you design! At the end of this task, we will ask you to implement some model query functions using your design, and answer the questions in the PLAN file about your design decisions.

Choosing Collections

It’s likely you will need to store groups of objects so you can organize and access them at a later date. Java has a wide range of built-in data structures available in java.util.Collections, but for simplicity we restrict the ones you may use to those we have discussed at length in this course. In particular, these are:

You may not use any other types of Collection to complete this assignment.

Consider the relative merits of each type of data structure and the uses for which they are appropriate. You should use collections whose properties correspond to the properties of the data they will be storing. For example, if your data has a meaningful ordering and permits duplicates, a sorted list might be a good way of representing that order. You should write similar justifications for the data structures you use in your PLAN.

Make sure the static types you declare for variables and method parameters are interfaces rather than implementations. That is, write List<T>, Map<K, V>, etc., rather than LinkedList<T> or TreeMap<K, V>. Doing so is good practice as it makes your code easier to maintain and update—if you choose to change the implementing class you can simply replace the constructor call without having to make any other changes.

Creating New Classes

You may find it useful to create new classes to represent certain components of the server state. Classes are a useful organizational tool because they associate state and functionality in small, discrete units. The design principle of separation of concerns is important to keep in mind; the internal details of a particular operation can be encapsulated within a class to hide details other classes don’t need to know about. Feel free to create as many or as few new classes (and/or interfaces) as you deem necessary, and to extend others as you wish. Just make sure to add Javadoc comments in all new files and classes you create to describe what their purpose is!

Here is some useful advice to keep in mind if you end up creating new classes.

Implementing User Models

Now it’s time to implement some model query functions based on your design. If you have determined you will need to make additional classes, you should have at least a skeletal implementation before continuing. You should add whatever collections you plan on using as private fields in your ServerModel class, and ensure you are initializing them in your constructor.

You should now implement the getRegisteredUsers method in the ServerModel class, using the collections you’ve chosen for storing information about the clients currently connected to the server.

If you’re unfamiliar with a class or method, you should read the Javadocs to get a sense of how it can be used. These HTML pages are generated from the /** ... */ comments before a class or method definition, which are often referred to as Javadoc comments. Here’s a link to the Javadocs for getRegisteredUsers.

You should also implement the getUserId and getNickname methods. If you find your logic for any of these methods to be rather convoluted, you should consider whether an alternative design for your ServerModel state is more appropriate. If you need to change your design, doing so now rather than later will save you many headaches!

Implementing Channel Models

Once you have the ability to model the users connected to the server, you will also need to store the channels on the server, and the users they contain. After adding the necessary collections, you should implement the getChannels, getUsersInChannel, and getOwner methods in ServerModel. You may need to make some additional modifications to your model at this point—remember to document them in your PLAN.

Checkpoint 2

At this stage, you should have:


Task 3: Connections and Nicknames

The next ServerModel features you will implement are acknowledging user connections and allowing users to set their own nicknames. We’ll first introduce the (provided) Broadcast class, which is used by the server to coordinate responses to clients. Then it will be your turn to implement some of those responses.

Generating Broadcast Objects

While the server receives commands from one client at a time, it is often the case that multiple clients should be informed about the effects of a command. For instance, when a client changes his or her nickname, everybody who can see that client should be informed of the name change, not just the user whose nickname changed.

To facilitate the sending of multiple responses at once, the ServerModel and ServerBackend use the Broadcast class to queue a set of responses to be dispatched to potentially many clients (Note that a response is just a Command and a user to send that command to.). Given a Broadcast, the ServerBackend will take care of sending the appropriate protocol responses to the clients. (Note the separation of concerns here—the ServerModel does not need to know the intricacies of the protocol, only the public interface for the Broadcast class).

As you will see in the Javadocs for Broadcast, you cannot create a Broadcast simply by calling the constructor. Instead, the class provides a set of static factory methods, which can be called to create the appropriate type of Broadcast. Here are the conditions under which you should use each factory method:

Interpreting Broadcast Test Output

We’ve provided you with several tests that test various components of your chat server. Soon, if not already, you will write your own tests for your server model. If an assert statement compares an expected and actual broadcast, assertEquals(expected, actual), and finds that they are not equal, the actual and expected Broadcasts will be printed out as part of the JUnit test output.

Here’s an example of a test output:

broadcast expected:<{User0=[:User1 JOIN java], User1=[:User1 JOIN java, :User1 NAMES java :@User0 User1]}> but was:<{User0=[:User1 JOIN java], User1=[:User1 NAMES java :@User0]}>.

The test output displays who the Broadcast is sent to and what messages they receive. The assert statement that produced the output shown above expected a Broadcast would be sent to the users with nicknames “User0” and “User1”. The expected Broadcast would tell User0 and User1 different things. User0 would be told that User1 joined the channel “java”: User0=[:User1 JOIN java]. User1 would be told two things. First, that User1 joined the channel “java”, and second, that the names of the users in the channel “java” are “User0” and “User1”: User1=[:User1 JOIN java, :User1 NAMES java :@User0 User1]. Note that the NAMES part of the message puts an @ before “User0”. The @ designates User0 as the owner of the channel “java”.

The actual Broadcast was: {User0=[:User1 JOIN java], User1=[:User1 NAMES java :@User0]. How is this different from what the test expected? In this case, the NAMES part of the Broadcast meant for User1 did not include User1. Now you know that the JoinCommand is producing a Broadcast that doesn’t include User1 in its set of recipients, and you can use that information to start debugging your code!

Handling Client Registration

The registerUser method will be called by the server backend when a new client connects to the server, passing the ID of the new client as an argument. Because the client has just connected to the server, they have not yet had the chance to set their nickname, and so you should use the provided generateUniqueNickname method to assign them a default nickname. You should store the association between user ID and nickname in your model’s data structures.

This function will return a Broadcast object to the server backend. You should use the connected static method to construct a broadcast containing the client’s initially assigned nickname. Once this is done, your implementation should pass some of the simpler tests in ConnectionNicknamesTest.java.

The only recipient for this broadcast will be the user who just registered.

Handling Client Disconnection

When a client disconnects from the server, the server backend will call the deregisterUser method, instructing the client to remove all state associated with the user who has quit (if they later rejoin, they should do so as an entirely new user). There might also be other clients who were in the same channels as this user; they should be notified that the user has quit. (The user who quit cannot be sent any such notification, since they are no longer in contact with the server.)

You will want to construct a broadcast using the disconnected static method. This method takes the nickname of the user who just quit the server, as well as a set of nicknames of users who were in the same channels as the disconnected user. (This set will be empty for now, since you have not yet implemented channel creation).

If the user that just quit the server was the owner of any channels, those channels should be deleted.

The recipients of this broadcast should be all the users who were in a channel with the disconnecting user.

Handling Nickname Changes

The next step is allowing users to change their nicknames. As will be the case when implementing most commands, you will need to distribute work between the updateServerModel method of the corresponding Command and the ServerModel itself. In this case, you will need to modify the updateServerModel method of the NicknameCommand class.

Your updateServerModel methods should never directly modify the data structures and fields of ServerModel (this should not even be possible!).

The table below contains a specification of the errors (defined in the ServerResponse enum) that might arise as a result of handling an invalid NicknameCommand. If such an error occurs, you should immediately return a Broadcast object created using the error static method. It is important to detect such errors as early as possible, so that the state of the server is not corrupted as a result of partially performing invalid commands.

Error conditions for NicknameCommand
NAME_ALREADY_IN_USE There is already a user with the desired nickname.
INVALID_NAME The desired nickname contains illegal characters.

You can use the isValidName method in ServerModel to validate a nickname.

If the command can be handled successfully by the model, then you should relay the command to any clients in the same channels as the sender by using the okay static method in Broadcast, including the user who changed their name. Note that this method takes a Command as an argument. Since we are creating a Broadcast as a result of handling the current command, you can pass this (a reference to the current object instance) as the command.

Checkpoint 3

At this stage, you should have:


Task 4: Channels and Messages

In this task, you will implement some more commands related to channel creation, entry and exit, and messaging. By the end, you will have a working implementation of most of the server functionality!

Handling Channel Creation

In this step, you will add support for clients creating new channels on the server. As in earlier tasks, you will have to coordinate channel creation between the updateServerModel method of the CreateCommand and the ServerModel itself.

Recall that every channel has a name, an owner, and a set of users who are in the channel. (For now, you can ignore the inviteOnly parameter; it will be used later.) When handling a CreateCommand, you should make sure to store this information in your ServerModel. If the channel was successfully created, you should inform only the channel’s creator using the okay factory method in Broadcast. Only the creator of the channel should be in the set of users receiving the response.

If an error creating the channel arises, you should return a Broadcast containing the appropriate error. Use the same isValidName method to validate a channel name.

Error conditions for CreateCommand
CHANNEL_ALREADY_EXISTS There is already a channel with the desired name.
INVALID_NAME The desired channel name contains illegal characters.

Adding Users to Channels

If a channel already exists, a client should be able to join it by issuing a JoinCommand. If no errors arise, then the client should be added to the channel, and the users already present in the channel should be notified that a new user has joined.

How does the new joiner know the names of everyone already in the channel? This is handled by the names method in Broadcast, which generates an additional protocol message to the client with the nicknames of the channel’s users. You should consult the Javadocs for names for more information about this method.

In task 5, you will implement invite-only channels, which require users to be invited by the channel’s owner instead of joining freely. This added feature means that the code you write now will need to be extended later on. Writing a clear and well-factored implementation now will make this much easier. For now, you should ignore the JOIN_PRIVATE_CHANNEL error.

The recipients for this Broadcast should be all the people in the channel that the user just joined.

Error conditions for JoinCommand
NO_SUCH_CHANNEL There is no channel with the specified name.
JOIN_PRIVATE_CHANNEL The channel is private, and the user has not been invited.

What if the client tries to join a channel they are already a part of? The resulting model state should be no different than the original, which means we do not need to protect the server state from this sort of error. In cases like this, it is safe to “ignore” the error and process the command as usual.

Sending Messages to Channels

Finally, we are ready to implement the actual messaging part of the server! When a user sends a message to a channel, it should be relayed (via an okay Broadcast) to all clients in that channel. You should keep in mind that no part of the server model needs to change for message delivery; you should rely on it only for error condition checking.

Error conditions for MessageCommand
NO_SUCH_CHANNEL There is no channel with the specified name.
USER_NOT_IN_CHANNEL The user is not in the specified channel.

Users Leaving Channels

At any time, a user may decide to leave a channel by issuing a LeaveCommand. This will prevent them from receiving any further messages from that channel. If the command is okay, all users in the channel should be added to the Broadcast you create. This includes the user who has left the channel.

Because every channel has exactly one owner (and the owner cannot change), we specify that if the owner of a channel leaves, every user is to be removed from the channel, and the channel itself should be destroyed. You should issue a Broadcast the same way as before to the same group of recipients, including the owner of the channel; your handling of the command in the ServerModel is the only place where you will need to account for this case.

Error conditions for LeaveCommand
NO_SUCH_CHANNEL There is no channel with the specified name.
USER_NOT_IN_CHANNEL The user is not in the specified channel.

Checkpoint 4

At this stage, you should have:


Testing your Server Using the Client

Running the Client

At this point, your server supports enough functionality to be used as a real chat server! We’ve provided you with a client application in the file hw07-client.jar (which is already included in the project files in Codio and can be downloaded separately if you’re using Eclipse).

Once the client is running, you will be prompted to enter the IP address of the server. Since the client and the server are running on the same computer (either yours or Codio’s virtual computer), the address you should enter is localhost. Note that the client will not display all of the channels on the server—to populate the list on the left-hand side, you will either have to create or join a channel.

Advice about Debugging

One caveat: although testing different interactions in the client is a good way to test a range of your server’s behaviors, it is not a replacement for writing JUnit tests. There may be (unintentional) bugs in the client application, and it is not possible to exhaustively test all cases using the client UI. If you encounter a bug while using the client, it is best to translate the sequence of steps you followed into a JUnit test. The TAs are not responsible for helping you debug behavior in the client, unless there is a corresponding JUnit test case.


Task 5: Invite-Only Channels

In this task, you will extend the basic functionality of your chat server with additional features for private, invite-only channels. If your model design in ServerModel does not yet include information about whether a channel is invite-only, you should add that before moving on.

Once your model is updated to take this information into account, you should go back to your implementation of JoinCommand and add error detection for the case of JOIN_PRIVATE_CHANNEL. As you proceed through the remaining steps, if you find duplicated functionality or other suboptimality in your models, you should consider refactoring your implementation.

Inviting Users to Channels

The InviteCommand is the equivalent of JoinCommand for invite-only channels. Users are added directly by the channel’s owner. You should use the names method to inform the joinee of the names of the other users in the channel.

You will notice that there are multiple error messages that might result from an improper command (e.g., inviting a non-existent user to a public channel). In such cases, it is acceptable to return a Broadcast with any one of the appropriate errors.

Error conditions for InviteCommand
NO_SUCH_USER There is no target user with the specified name.
INVITE_TO_PUBLIC_CHANNEL The specified channel is public.
NO_SUCH_CHANNEL There is no channel with the specified name.
USER_NOT_OWNER The sender is not the owner of the specified channel.

Kicking Users from Channels

Our final extension will be supporting the KickCommand, with which a channel’s owner can remove a user from the channel. This command should be supported for both public and private channels. Like with the LeaveCommand, if the owner kicks themself out of a channel, you should also remove the channel from your server state entirely.

The recipients for this Broadcast should be all the people in the channel that the user just left, including the kicked user.

Error conditions for KickCommand
NO_SUCH_USER There is no target user with the specified name.
USER_NOT_OWNER The sender is not the owner of the specified channel
NO_SUCH_CHANNEL There is no channel with the specified name.
USER_NOT_IN_CHANNEL The user is not in the specified channel.

Checkpoint 5

At this stage, you should have:


Task 6: Refactoring

Now that you have implemented all the required features, it is important to take some time to look for any redundant data, convoluted functions, or any other issues that can be refactored into more elegant code. It is very likely that some aspect of your initial design is not completely optimal, so don’t be afraid to adjust some data structures, split functions into multiple helper functions, and so on. Make sure to document any changes you make in your PLAN file.

Checkpoint 6

You should now have completed any necessary refactoring work. Ensure that your code complies with the CIS 120 Java style guidelines and that it is properly organized and documented.


Broadcast toString

It’s important to understand the output of the toString method of Broadcast - it returns a list of responses. Each response will have a command attached to it in caps. View ClientCommand.java for a list of the commands. To help understand this, we’ve provided a few examples below.

{User0=[:User0 CONNECT]} means that User0 connected. The key, “User0”, is the user who sent the broadcast.

{cis120=[:User0 NICK cis120]} means User0 changed his nickname to CIS120

{User0=[:User0 ERROR 500]} indicates an error. Specifically, a NAME_ALREADY_IN_USE error (refer to ServerResponse code 500).


Submission and Grading

Before submitting your assignment, you should take one last look through your PLAN file to see if there are any questions you haven’t yet answered, or update your answers if necessary.

Submission Instructions

As with previous assignments, you will be uploading hw07-submit.zip, a compressed archive containing only the following files:

If you are using Codio

These files should be organized similar to above (with a src and test directory). The easiest option is to use the Zip menu item in Codio. Do not include any of the other provided files, since doing so may cause your submission to fail to compile.

If you are using Eclipse

Alternative 1 - Zip your files from Eclipse using the instructions below.

Follow these instructions to create and upload hw07-submit.zip:

Alternative 2 - Copy-Paste your code in Codio and zip from there.

You have three free submissions for this assignment. Each additional submission will cost you five points from your final score.

Grading Breakdown