pennpals

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.

How to Approach this Project

This project, like the OCaml Paint project, is designed to get you comfortable working with large 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 related components, and you will probably find it rewarding to maintain a fair amount of stylistic discipline. A poorly named datastructure, 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

Create a new Java project in your Eclipse workspace, and remember to add JUnit as a library. Download and import all of the .java files into your src directory in Eclipse. Make sure to keep the README.txt file handy as well.


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 (including 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 any given 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, then the owner of the channel must add other users by sending an InviteCommand. For simplicity’s sake, the owner of a channel cannot be changed; a channel is removed if its owner leaves.

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

PennPals Chat Protocol

The PennPals protocol is modeled around a set of commands which 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 the sender wishes the server to perform.

The table below is a general summary of each of the commands that can be issued by a client:

CommandEffect
NicknameCommand Changes the sender’s nickname.
CreateCommand Creates a new channel with the sender as its owner.
JoinCommand Adds the sender to a public channel.
InviteCommand Invites the target user to a private channel owned by the sender.
MessageCommand Delivers a message to all users in a channel.
LeaveCommand Removes the sender from a channel.
KickCommand Removes the target user from a private channel.

How Commands are Represented

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 recieves a command string from a client, it uses 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.

You don’t need to be concerned with the specifics of converting command strings to objects (since we provide you with the parser), though you will likely see them printed to the console when you run your server.

Subtyping and Dynamic Dispatch

All of the possible protocol commands are subtypes of the abstract class Command . Since Command is abstract, it cannot be directly instantiated. Unlike an interface, however, the abstract class defines some fields and methods which are inherited by its concrete subclasses.

In this case, Command is designed to be extended by classes representing each of the different commands defined by the protocol. Every instance of Command has implementations for at least the following methods:

  • int getSenderId()
  • String getSender()
  • Broadcast updateServerModel(ServerModel model)

The updateServerModel method is marked abstract in the definition of Command, meaning that each subclass must have its own implementation of that method. This allows the server backend, regardless of the command’s dynamic class, to call updateServerModel and rely on dynamic dispatch to achieve 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.


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!
In later steps, you will need to add public methods to the ServerModel that let commands appropriately modify the ServerModel state.

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 implementaton 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 just 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 README 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 we restrict the ones you may use to those we have discussed at length in this course. In particular, these are:

  • TreeSet: Implementation of the Set interface using BSTs, like the BSTSet you created in OCaml for Homework 3. Elements placed into this collection must implement the Comparable interface (see next section).
  • TreeMap: Implementation of the Map interface using BSTs. Keys must implement Comparable interface. Similar in structure and efficiency to TreeSet, but can be used to assocate values with keys.
  • LinkedList: Implementation of the List and Deque interfaces using a doubly-linked list. This is equivalent to the deques you implemented as part of Homework 4.

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 README.

Make sure your static types are interfaces rather than implementations. 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 as you deem necessary, and extend or implement whatever other classes or interfaces you wish.

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

  • Designate your class fields as private. Controlling access to the internal fields of your class prevents other classes and users from relying on access to its internal state or breaking invariants. You should use getter and setter methods to query and update the internal state of your class, and ensure that those methods maintain the invaraints. This allows you to encapsulate private state and mantain separation between components of your program.

  • Override the equals method to define equality for instances of your class. By default, new objects inherit the equals method from Object, which is simple referential equality. If you would like a more meaningful structural comparison, you are responsible for defining it, usually in terms of the data stored within the fields of the class. In addition to the equals method, it is also good practice to override the hashCode method to be consistent with equals.

  • Consider having your class implement the Comparable interface. The TreeSet and TreeMap data structures rely on their elements having a natural ordering, which can be used to achieve efficient lookup of elements. Although most built-in Java types already implement Comparable, if you would like to store a custom class in one of these data structures, you must impement Comparable<T> for your class T. Implementing this interface requires you to define the method compareTo according to the specification in the Javadocs for Comparable.

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, getUsers, 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 README.

Fill out the portion of the README file corresponding to task 2, which requires you to document your design process and justify the decisions you made. Though you cannot yet test the query functions in ServerModel, you should complete them before moving on.

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. 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:

  • Use Broadcast.connected when a new client connects. This will be used to inform the client of their initially assigned nickname.

  • Use Broadcast.disconnected when a client disconnects from the server. This will be used to inform other users in that client’s channels that they have quit the server. Note that because the connection has closed, the original client cannot be sent this Broadcast.

  • Use Broadcast.names when adding a client to a channel. This Broadcast should only be sent as a result of handling a JoinCommand or an InviteCommand. It will inform all clients that a new user has been added to the channel in question, and also inform the new user of the names of everybody already in the channel.

  • Use Broadcast.error when processing a command results in an error. The sender who issued an invalid command (and no other clients) should be informed that their command resulted in an error. There is a specified set of error conditions for each command type, which we’ll introduce as they arise.

  • Use Broadcast.okay in all other cases where the command is handled successfully. This method can should be used to instruct all clients to perform whatever command was issued by the sender of the command.

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 be passing some of the simpler tests in ConnectionNicknamesTest.java.

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 they have 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.)

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!). Instead, you should add public methods to ServerModel which can be used to update the state as a result of performing certain actions.

For instance, you might consider adding a changeNickname method to your ServerModel class, which you can then call from the updateServerModel method in NicknameCommand. A similar approach might be helpful for implementing subsequent commands.

The table below contains a specification of the errors (defined in the ServerError 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. 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.

At this stage, your implementation should be passing all tests in ConnectionNicknamesTest.java. User registration, deregistration, and nickname changes should all be functioning correctly. If you made any changes to your design while completing this task, you should document this in your README.

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 before, 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 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.

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 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; your handling of the command on 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.
You should be able to create, join, and leave channels, and send messages. In particular, your implementation should pass all tests in ChannelsMessagesTest.java. The chat client should be mostly functioning, and you should be able to run multiple instances to test their interactions.

Testing your Server Using the Client

Running the Client JAR

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. Before launching the client, fire up your server by running the file ServerMain.java in Eclipse. You should see a small window pop up which indicates the server is running.

Now, launch an instance of the client, either by double-clicking on the JAR file, or selecting Open With... JAR launcher (or some equivalent program). If you are unable to run the client, try opening the directory containing the client JAR in a terminal, and executing the command java -jar hw07-client.jar. You will need to repeat this process to test your server with multiple instances of the client application.

Once the client is running, you will be prompted to enter the IP address of the server. Since you are running the server on your own 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 LeaveCommand, if the owner kicks themself out of a channel, you should also remove the channel from your server state entirely.

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.
Your implementation should now pass all the provided test cases, now including InviteOnlyTest.java. Your server should be fully functional, so fire up two instances of the client application and spend some time playing around with it. (Make sure to fix any bugs you find!)

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 README file.

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.

Submission and Grading

Before submitting your assignment, you should take one last look through your README 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 files.zip, a compressed archive containing only the following files:

  • README.txt
  • Command.java
  • ServerModel.java
  • ServerModelTest.java
  • Any additional classes you made

These files should be at the root of the archive file; make sure you compress the files directly, rather than compressing a directory containing the files. Do not include any of the other provided files, since doing so may cause your submission to fail to compile.

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

Grading Breakdown

  • Automated testing (80%)
    • Task 3: Connections and Nicknames (30%)
    • Task 4: Channels and Messages (25%)
    • Task 5: Invite-Only Channels (20%)
    • Model state encapsulation (5%)
  • Manual grading (20%)
    • README.txt (5%)
    • Style and design (10%)
    • Quality of testing (5%)