Created
January 13, 2014 18:01
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why have you used "currentState == InterpreterState.TRANSACTION" twice in the same if else statements?
Isn't it redundant?