Skip to content

Instantly share code, notes, and snippets.

@TosinAF
Created January 13, 2014 18:01
Show Gist options
  • Save TosinAF/8404969 to your computer and use it in GitHub Desktop.
Save TosinAF/8404969 to your computer and use it in GitHub Desktop.
Code Sample from my implementation of a POP3 Mail Server (https://github.com/TosinAF/POP3MailServer) The POP3 Mail Server is split into several classes (Command Interpreter, Database, Server). This code sample shows my excellent knowledge of OOP, appropriate use of comments and ability to write tests for my code.
/**
* The Class Command Interpreter.
*
* @author Tosin Afolabi
* @version 1.0
*
* This class will receive commands from the network Call the
* approaiate method on the Databse then send a response back to the
* client
*
* It will support the following commands USER, PASS, QUIT, STAT, LIST,
* RETR, DELE, NOOP, RSET, TOP, & UIDL
*
* Uses a Mock db Backend that implements the IDatabase Interface
*
* It follows the specfication written by J.Myers Of Carnegie Mellon
*
* **Added a few more methods to check whether database connection is active
*/
public class CommandInterpreter implements ICommandInterpreter {
private Database db;
private String usernameToAuthWithPassword;
private boolean previousCommandWasASuccessfulUSERAuth;
private InterpreterState currentState;
public enum POP3Command {
USER, PASS, QUIT, STAT, LIST, RETR, DELE, NOOP, RSET, TOP, UIDL, ERROR;
}
public enum InterpreterState {
AUTHORIZATION, TRANSACTION, UPDATE;
}
public CommandInterpreter() {
db = new Database();
usernameToAuthWithPassword = "";
previousCommandWasASuccessfulUSERAuth = false;
currentState = InterpreterState.AUTHORIZATION;
}
public boolean checkInitialStatus() {
return db.wasConnectionStarted();
}
public void closeInterpreter() {
db.closeConnection();
}
public String handleInput(String input) {
if (!db.isConnectionValid()) return "-ERR Database Connection Terminated Unexpectedly";
String response = "";
// Remove Extra Whitespace
String trimmedInput = input.trim();
// Split Input into Arguements
String[] arguementsArray = trimmedInput.split(" ");
// Check that no arguement is greater then 40 characters
if (!lengthOfArguementsIsWithinLimit(arguementsArray)) {
return "-ERR One of the arguements in the command is longer than 40 characters";
}
// Number Of arguements, not including the command keyword, e.g USER
int numberOfArguements = arguementsArray.length - 1;
POP3Command command = setPOP3CommandEnum(arguementsArray[0]);
if (currentState == InterpreterState.AUTHORIZATION) {
switch (command) {
case USER:
/*
* Provides username to the POP3 server. Must be followed by a
* PASS command.
*
* @arguements - a string identifying a mailbox (required)
*/
if (numberOfArguements != 1)
return "-ERR Syntax -> USER mailboxName(required)";
String username = arguementsArray[1];
response += authenticateUsername(username);
break;
case PASS:
/*
* Provides a password to the POP3 server. Must follow a
* Successful USER command.
*
* @arguements - a server/mailbox-specific password (required)
*
* Spaces in the argument are treated as part of the password,
* instead of as argument separators.
*/
if (input.length() < 6)
return "-ERR Syntax -> PASS password(required)";
response += authenticatePassword(input);
previousCommandWasASuccessfulUSERAuth = false;
break;
case QUIT:
/*
* Ends the POP3 session
*
* @arguements - none
*/
if (numberOfArguements != 0)
return "-ERR This command has no arguements";
response += "+OK POP3 Server Connection Terminated";
usernameToAuthWithPassword = null;
previousCommandWasASuccessfulUSERAuth = false;
break;
case ERROR:
/*
* Unsupported Commands, e.g APOP
*/
response += "-ERR Unsupported Command Given";
previousCommandWasASuccessfulUSERAuth = false;
break;
default:
/*
* For all other supported commands, e.g LIST, RETR that are not
* allowed to run in AUTHROZIATION STATE
*/
response += "-ERR User needs to be authenticated";
previousCommandWasASuccessfulUSERAuth = false;
break;
}
} else if (currentState == InterpreterState.TRANSACTION) {
int messageNumber = -1;
// If the command has arguements, then arguementsArray[1] holds the
// message number
if (numberOfArguements > 0 && command != POP3Command.USER
&& command != POP3Command.PASS) {
if (stringCanBeParsedAsAnInteger(arguementsArray[1])) {
messageNumber = Integer.parseInt(arguementsArray[1]);
} else {
return "-Err Invalid Arguement Type";
}
}
switch (command) {
case USER:
case PASS:
response += "-ERR maildrop already locked";
break;
case STAT:
/*
* Returns the number of messages and total size of mailbox.
*
* @arguements - none
*/
if (numberOfArguements > 0)
return "-ERR This command has no arguements";
response += getDropListing();
break;
case LIST:
/*
* Lists message number and size of each message. If a message
* number is specified, returns the size of the specified
* message.
*
* @arguements - messageNumber (optional)
*/
if (numberOfArguements > 1)
return "-ERR Syntax -> LIST messageNumber(optional)";
response += getScanListing(numberOfArguements, messageNumber);
break;
case RETR:
/*
* Returns the full text of the specified message, and marks
* that message as read.
*
* @arguements - messageNumber (required)
*/
if (numberOfArguements != 1)
return "-ERR Syntax -> RETR messageNumber(required)";
response += getMessageBody(messageNumber);
break;
case DELE:
/*
* Marks the specified message for deletion.
*
* @arguements - messageNumber (required)
*/
if (numberOfArguements != 1)
return "-ERR Syntax -> DELE messageNumber(required)";
response += markMessageForDeletion(messageNumber);
break;
case NOOP:
/*
* Returns a simple acknowledgement, without performing any
* function.
*
* @arguements - none
*/
if (numberOfArguements != 0)
return "-ERR This command has no arguements";
response += "+OK";
break;
case RSET:
/*
* Umarks all Messages Marked For Deletion
*
* @arguements - none
*/
if (numberOfArguements != 0)
return "-ERR This command has no arguements";
db.unmarkAllMessagesMarkedForDeletion();
response += "+OK maildrop has "
+ db.getNumberOfMailsInMaildrop() + " ("
+ db.getSizeOfMaildrop() + ") octets";
break;
case TOP:
/*
* Returns the specified number of lines from the specified
* mesasge number.
*
* @arguements - messageNumber, numberOfLines(non negative)
* (both required)
*/
if (numberOfArguements != 2)
return "-ERR Syntax -> TOP messageNumber, numberOfLines(non negative) (both required)";
int numberOfLines = -1;
if (stringCanBeParsedAsAnInteger(arguementsArray[2])) {
numberOfLines = Integer.parseInt(arguementsArray[2]);
} else {
return "-Err Invalid Arguement Type\n\n";
}
if (numberOfLines >= 0) {
response += getXNumberOfLinesInMessageBody(messageNumber,
numberOfLines);
} else {
response += "-ERR Non-Negative Number must be given with this command";
}
break;
case UIDL:
/*
* Returns the UniqueID Listing of a Message or all messages
*
* @arguements - messageNumber (optional)
*/
if (numberOfArguements > 1)
return "-ERR Syntax -> UIDL messageNumber(optional)";
response += getUniqueIDListing(trimmedInput, messageNumber);
break;
case QUIT:
/*
* Ends the POP3 session & Changes State to UPDATE
*
* @arguements - none
*/
if (numberOfArguements != 0)
return "-ERR This command has no arguements";
currentState = InterpreterState.UPDATE;
if (db.deleteAllMessagesMarkedforDeletion()) {
response += "+OK Messages Deleted, POP3 Server Connection Terminated";
} else {
response += "-ERR Some messages marked for deletion were not removed";
}
break;
case ERROR:
default:
/*
* Unsupported Commands, e.g APOP
*/
response += "-ERR Unsupported Command Given";
break;
}
} else if (currentState == InterpreterState.TRANSACTION) {
response += "-ERR The connection to the mail server has been terminated";
}
return response;
}
private String authenticateUsername(String username) {
if (db.authenticateUser(username)) {
usernameToAuthWithPassword = username;
previousCommandWasASuccessfulUSERAuth = true;
return "+OK name is a valid mailbox";
} else {
previousCommandWasASuccessfulUSERAuth = false;
return "-ERR never heard of mailbox name";
}
}
private String authenticatePassword(String input) {
if (previousCommandWasASuccessfulUSERAuth) {
String password = input.substring(5);
if (db.authenticatePassword(usernameToAuthWithPassword,
password)) {
currentState = InterpreterState.TRANSACTION;
return "+OK maildrop locked & ready";
} else {
return "-ERR Invalid Password";
}
} else {
return "-ERR A successful USER Command needs to be run immediately before";
}
}
private String getDropListing() {
return "+OK " + db.getNumberOfMailsInMaildrop() + " "
+ db.getSizeOfMaildrop();
}
private String getScanListing(int numberOfArguements, int messageNumber) {
if (numberOfArguements == 0) {
// Command contains No Arguements
if (db.getNumberOfMailsInMaildrop() == 0) {
return "+OK no messages in maildrop";
} else {
return "+OK scan listing follows\n"
+ db.getScanListingOfAllMessages();
}
} else {
// Command contains Arguements
if (db.getAccessStatusOfMessage(messageNumber)) {
return "+OK " + db.getScanListingOfMessage(messageNumber);
} else {
return "-ERR Message not found or has been marked for deletion";
}
}
}
private String getMessageBody(int messageNumber) {
if (db.getAccessStatusOfMessage(messageNumber)) {
return "+OK " + db.getSizeOfMessageInOctets(messageNumber)
+ " octets\n" + db.getMessageBody(messageNumber);
} else {
return "-ERR Message not found or has been marked for deletion or invalid arguement";
}
}
private String getXNumberOfLinesInMessageBody(int messageNumber,
int numberOfLines) {
if (db.getAccessStatusOfMessage(messageNumber)) {
return "+OK \n"
+ db.getXNumberOfLinesInMessageBody(messageNumber,
numberOfLines);
} else {
return "-ERR Message not found or has been marked for deletion";
}
}
private String markMessageForDeletion(int messageNumber) {
if (db.getAccessStatusOfMessage(messageNumber)) {
db.markMessageForDeletion(messageNumber);
return "+OK Message successfully marked for deletion";
} else {
return "-ERR Message not found or has already been marked for deletion";
}
}
private String getUniqueIDListing(String trimmedInput, int messageNumber) {
if (trimmedInput.length() == 4) {
// Command contains No Arguements
return "+OK Uniquie ID listing follows\n"
+ db.getUniqueIDListingOfAllMessages();
} else {
// Command contains Arguements
if (db.getAccessStatusOfMessage(messageNumber)) {
return "+OK "
+ db.getUniqueIDListingOfMessage(messageNumber);
} else {
return "-ERR Message not found or has been marked for deletion or invalid arguement";
}
}
}
/*
* Checks that each arguement does not exceed a character count of 40
* Returns True, if none of the arguements exceeds the limit. False, if not.
*/
private boolean lengthOfArguementsIsWithinLimit(String[] arguementsArray) {
for (String arguement : arguementsArray) {
if (arguement.length() > 40)
return false;
}
return true;
}
/*
* Matches a given command string to a particular POP3CommandEnum If not
* possible, it is set to the POP3Command Error Enum
*/
private POP3Command setPOP3CommandEnum(String command) {
try {
command = command.toUpperCase();
return POP3Command.valueOf(command);
} catch (Exception IIlegalArguementException) {
return POP3Command.ERROR;
}
}
private boolean stringCanBeParsedAsAnInteger(String number) {
try {
Integer.parseInt(number);
} catch (NumberFormatException e) {
return false;
}
return true;
}
}
import org.junit.*;
import static org.junit.Assert.*;
/**
* The Test Class CommandInterpreterTest.
*
* @author Tosin Afolabi
*
* Functional Regressive Tests for the Command Interpreter(CI) Class The
* Main Purpose of the (CI) is to accurately parse the input given by
* users And Respond Accordingly.
*
* Thus as long as the commands are interpreted correctly & the
* appropriate response is given we can be confident that the
* application is working as intended even though the code coverage may
* not be at 100%
*/
public class CommandInterpreterTest {
CommandInterpreter ci;
@Before
public void setUp() {
ci = new CommandInterpreter();
}
@Test
public void testInterpreterStateAuthorization() {
// Test using lower case commands are recognized aswell
assertEquals("-ERR Syntax -> USER mailboxName(required)",
ci.handleInput("user"));
// Missing Arguements
assertEquals("-ERR Syntax -> USER mailboxName(required)",
ci.handleInput("USER"));
assertEquals("-ERR Syntax -> PASS password(required)",
ci.handleInput("PASS "));
// Unkown User
assertEquals("-ERR never heard of mailbox name",
ci.handleInput("USER tosin"));
assertEquals(
"-ERR A successful USER Command needs to be run immediately before",
ci.handleInput("PASS opebi"));
// Known User, Wrong Password
assertEquals("+OK name is a valid mailbox",
ci.handleInput("USER test"));
assertEquals("-ERR Invalid Password",
ci.handleInput("PASS wrongPassword"));
// Unsupported Command
assertEquals("-ERR Unsupported Command Given",
ci.handleInput("APOP test hbfdhjbjhfbjhdfjfh"));
// Quit Command in Authorization State
assertEquals("+OK POP3 Server Connection Terminated",
ci.handleInput("QUIT"));
// User & Password Authenticated
assertEquals("+OK name is a valid mailbox",
ci.handleInput("USER test"));
assertEquals("+OK maildrop locked & ready",
ci.handleInput("PASS password"));
}
@Test
public void testInterpreterStateAuthorization2() {
// None of these commands are allowed in the authorization stage
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("STAT"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("LIST 1"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("RETR 1"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("DELE 1"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("NOOP"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("RSET"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("TOP 1 5"));
assertEquals("-ERR User needs to be authenticated",
ci.handleInput("UIDL 1"));
}
@Test
public void testCommandsInTransactionState() {
ci.handleInput("USER test");
ci.handleInput("PASS password");
// Command with an arugement longer than 40 characters
assertEquals(
"-ERR One of the arguements in the command is longer than 40 characters",
ci.handleInput("USER testtesttesttesttesttesttesttesttesttesttesttest"));
// USER Command
assertEquals("-ERR maildrop already locked",
ci.handleInput("USER test"));
// PASS Command
assertEquals("-ERR maildrop already locked",
ci.handleInput("pass password"));
// STAT Command
assertEquals("+OK 0 0", ci.handleInput("STAT"));
assertEquals("-ERR This command has no arguements",
ci.handleInput("STAT 1"));
// LIST Command
assertEquals("+OK scan listing follows\n",
ci.handleInput("LIST"));
// LIST Command With Arguements
assertEquals(
"-ERR Message not found or has been marked for deletion",
ci.handleInput("LIST 1"));
assertEquals("-ERR Syntax -> LIST messageNumber(optional)",
ci.handleInput("LIST 1 2"));
// Invalid Arguement Type
assertEquals("-Err Invalid Arguement Type",
ci.handleInput("LIST a"));
// RETR Command
assertEquals("-ERR Syntax -> RETR messageNumber(required)",
ci.handleInput("RETR"));
// RETR Command With Arguement
assertEquals(
"-ERR Message not found or has been marked for deletion or invalid arguement",
ci.handleInput("RETR 1"));
assertEquals("-ERR Syntax -> RETR messageNumber(required)",
ci.handleInput("RETR 1 2"));
// DELE Command
assertEquals("-ERR Syntax -> DELE messageNumber(required)",
ci.handleInput("DELE"));
// DELE Command With Arguement
assertEquals(
"-ERR Message not found or has already been marked for deletion",
ci.handleInput("DELE 1"));
// RSET Command
assertEquals("+OK maildrop has 0 (0) octets",
ci.handleInput("RSET"));
// RSET Command With Arguement - It should ignore the arguement
assertEquals("-ERR This command has no arguements",
ci.handleInput("RSET 1"));
// NOOP Command
assertEquals("+OK", ci.handleInput("NOOP"));
assertEquals("-ERR This command has no arguements",
ci.handleInput("NOOP 5"));
// TOP Command With No Arguements
assertEquals(
"-ERR Syntax -> TOP messageNumber, numberOfLines(non negative) (both required)",
ci.handleInput("TOP"));
// TOP Command
assertEquals(
"-ERR Syntax -> TOP messageNumber, numberOfLines(non negative) (both required)",
ci.handleInput("TOP 1"));
// TOP Command With Arguements
assertEquals(
"-ERR Message not found or has been marked for deletion",
ci.handleInput("TOP 1 5"));
// TOP Command With Last Arguement as a negative number
assertEquals(
"-ERR Non-Negative Number must be given with this command",
ci.handleInput("TOP 1 -5"));
// UIDL Command With No Arguements
assertEquals("+OK Uniquie ID listing follows\n",
ci.handleInput("UIDL"));
// UIDL Command With Arguement
assertEquals(
"-ERR Message not found or has been marked for deletion or invalid arguement",
ci.handleInput("UIDL 1"));
assertEquals("-ERR Syntax -> UIDL messageNumber(optional)",
ci.handleInput("UIDL 1 3"));
// Unsupported Command
assertEquals("-ERR Unsupported Command Given",
ci.handleInput("TOPP 1 -5"));
// Quit Command in Transaction State
// Mock Database returns false
assertEquals("-ERR This command has no arguements",
ci.handleInput("QUIT 1 2"));
assertEquals(
"-ERR Some messages marked for deletion were not removed",
ci.handleInput("QUIT"));
}
}
@nvsavant
Copy link

Why have you used "currentState == InterpreterState.TRANSACTION" twice in the same if else statements?
Isn't it redundant?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment