package admin; import java.sql.SQLException; import java.util.StringTokenizer; import utilities.*; /** * In the add modify module, the requirement strings the user builds are not in the native format * stored in the SAS database. This class acts as the bridge between the two formats, allowing * convienient conversion between the two formats. The format used in the database is referred to as * database format, and the format used in displaying the requirement to the user is referred to as * display format. Database format looks something like "1|(4#5)|^06" and display format looks * something like "CSC 175N or ( MTH 072A xor MTH 073A ) or all PHY". */ public class RequirementParser { /** * Converts a String in database format to display format. The numbers are assumed to be * course ids and disicpline ids (if a carrot appears in front). * @param in a String in database format * @return A String in display format semantically equal to the parameter string. * @exception SQLException If an error occured while connecting to the SAS database. */ public static String toCourseDisplayString(String in) throws SQLException { DatabaseReqStrTokenizer k = new DatabaseReqStrTokenizer(in); StringBuffer strBuf = new StringBuffer(); while(k.next()) { switch(k.getType()) { case TokenMap.AND: strBuf.append("and"); break; case TokenMap.OR: strBuf.append("or"); break; case TokenMap.XOR: strBuf.append("xor"); break; case TokenMap.LPAREN: strBuf.append("("); break; case TokenMap.RPAREN: strBuf.append(")"); break; case TokenMap.NUMBER: strBuf.append(Retriever.getCourseCodeFromCourseId( k.getValue())); break; case TokenMap.CARROT: switch(k.getDisciplineLevel()) { case 0: strBuf.append("all "); break; case 1: strBuf.append("lower "); break; case 2: strBuf.append("upper "); break; } strBuf.append(Retriever.getDisciplineCodeFromDisciplineId( k.getValue())); break; case TokenMap.LARTS: strBuf.append("liberal_arts"); break; } strBuf.append(" "); } return strBuf.toString(); } /** * Converts a String in display format to database format. The word tokens not * acting as operators are expected to be course codes and disciplines codes. * @param in A String in display format. * @return A String in database format semantically equal to the string passed in. * @exception SQLException if an error occurs while connecting to the SAS database. */ public static String toCourseDatabaseString(String in) throws SQLException { CourseDisplayReqStrTokenizer k = new CourseDisplayReqStrTokenizer(in); SyntaxChecker synChk = new SyntaxChecker(); StringBuffer strBuf = new StringBuffer(); while(k.next()) { int type = k.getType(); switch(type) { case TokenMap.OR: synChk.checkToken(type, "or"); strBuf.append("|"); break; case TokenMap.XOR: synChk.checkToken(type, "xor"); strBuf.append("#"); break; case TokenMap.AND: synChk.checkToken(type, "and"); strBuf.append("*"); break; case TokenMap.LPAREN: synChk.checkToken(type, "("); strBuf.append("("); break; case TokenMap.RPAREN: synChk.checkToken(type, ")"); strBuf.append(")"); break; case TokenMap.COURSE: synChk.checkToken(TokenMap.NUMBER, "course"); strBuf.append(Retriever.getCourseIdFromCourseCode(k.getValue())); break; case TokenMap.DISIPL: synChk.checkToken(TokenMap.CARROT, "discipline"); strBuf.append("^"); strBuf.append(k.getDisciplineLevel()); strBuf.append(Retriever.getDisciplineIdFromDisciplineCode(k.getValue())); break; case TokenMap.LARTS: synChk.checkToken(type, "liberal_arts"); strBuf.append("L"); break; } } synChk.finishChecking(); return strBuf.toString(); } /** * Converts a String in database format to display format. The numbers are expected * to be major ids. Carrot notation is not allowed (or even meaningful) in these * Strings. * @param in A String in database format. * @return A String in display format semantically equal to the string passed in. * @exception SQLException if an error occurs while connecting to the SAS database. */ public static String toMajorDisplayString(String in) throws SQLException { DatabaseReqStrTokenizer k = new DatabaseReqStrTokenizer(in); StringBuffer strBuf = new StringBuffer(); while(k.next()) { int type = k.getType(); switch(k.getType()) { case TokenMap.AND: strBuf.append("and"); break; case TokenMap.OR: strBuf.append("or"); break; case TokenMap.XOR: strBuf.append("xor"); break; case TokenMap.LPAREN: strBuf.append("("); break; case TokenMap.RPAREN: strBuf.append(")"); break; case TokenMap.NUMBER: strBuf.append(Retriever.getMajorCodeFromMajorId( k.getValue())); break; case TokenMap.CARROT: throw new RequirementParserException( "Carrot illegal in major string."); case TokenMap.LARTS: throw new RequirementParserException( "liberal_arts illegal in major string."); } strBuf.append(" "); } return strBuf.toString(); } /** * Converts a String in display format to database format. The word tokens not * acting as operators are expected to be major codes. * @param in A String in display format. * @return A String in database format semantically equal to the string passed in. * @exception SQLException if an error occurs while connecting to the SAS database. */ public static String toMajorDatabaseString(String in) throws SQLException { MajorDisplayReqStrTokenizer k = new MajorDisplayReqStrTokenizer(in); SyntaxChecker synChk = new SyntaxChecker(); StringBuffer strBuf = new StringBuffer(); while(k.next()) { int type = k.getType(); switch(type) { case TokenMap.OR: synChk.checkToken(type, "or"); strBuf.append("|"); break; case TokenMap.XOR: synChk.checkToken(type, "xor"); strBuf.append("#"); break; case TokenMap.AND: synChk.checkToken(type, "and"); strBuf.append("*"); break; case TokenMap.LPAREN: synChk.checkToken(type, "("); strBuf.append("("); break; case TokenMap.RPAREN: synChk.checkToken(type, ")"); strBuf.append(")"); break; case TokenMap.MAJOR: synChk.checkToken(TokenMap.NUMBER, "major"); strBuf.append(Retriever.getMajorIdFromMajorCode(k.getValue())); break; } } synChk.finishChecking(); return strBuf.toString(); } } /** * Tokenizes database strings and returns nicely packaged pieces of information extracted from * individual tokens. This class is used in tokenizing both major strings and course/discipline * strings. */ class DatabaseReqStrTokenizer { // holds the characters of the string passed to constructor private char[] reqStr; // current index into reqStr private int currIndex; // type of current token private int type; // the value of the current token private String tokenValue; // the disicpline level of the token (if it is indeed a disicpline) private int discipLevel; /** * Constructs an instance using the string passed in as the source of tokens. * @param req The string to tokenize. */ public DatabaseReqStrTokenizer(String req) { reqStr = req.toCharArray(); currIndex = 0; } /** * Reads an integer value out of the requirement string. Note it doesn't convert * it to an int, it just packages the characters found into another String. */ private void parseInteger() { tokenValue = ""; while(hasMore() && Character.isDigit(reqStr[currIndex])) { tokenValue += reqStr[currIndex]; currIndex++; } } /** * Maps a token value to a numeric value indicating what type * of token it is. * @param ch the character whose type is determined. * @return The numeric token type of the character passed in. */ private static int getCharType(char ch) { switch(ch) { case 'L': return TokenMap.LARTS; case '|': return TokenMap.OR; case '*': return TokenMap.AND; case '(': return TokenMap.LPAREN; case ')': return TokenMap.RPAREN; case '^': return TokenMap.CARROT; case '#': return TokenMap.XOR; default: if(Character.isDigit(ch)) return TokenMap.NUMBER; else if(Character.isWhitespace(ch)) return TokenMap.SPACE; else return TokenMap.INVALID; } } /** * Skips characters in the internal requirement string until a non-whitespace * character is found. */ private void skipWhiteSpace() { while(hasMore() && Character.isWhitespace(reqStr[currIndex])) currIndex++; } /** * Internally used convience method to see if anymore characters * exist in the requirement string. */ private boolean hasMore() { return currIndex < reqStr.length; } /** * Checks to see if anymore tokens exist in the requirement string. If another does exist, * it is parsed in this method. * @return true if another token exists or false if not. */ public boolean next() { if(!hasMore()) return false; type = getCharType(reqStr[currIndex]); if(type == TokenMap.CARROT) { type = TokenMap.CARROT; currIndex++; parseInteger(); if(tokenValue.length() < 2) throw new RequirementParserException( "Requirement " + new String(reqStr) + " was invalid."); discipLevel = Character.digit(tokenValue.charAt(0), 10); tokenValue = tokenValue.substring(1); return true; } else if(type == TokenMap.NUMBER) { type = TokenMap.NUMBER; parseInteger(); return true; } else if(type == TokenMap.SPACE) { skipWhiteSpace(); return next(); } else if(type == TokenMap.INVALID) throw new RequirementParserException( "Requirement " + new String(reqStr) + " was invalid."); currIndex++; return true; } /** * Gets the type of the current token (ex. OR, XOR, AND, etc.) * @return the type of the current token. */ public int getType() { return type; } /** * Gets the value of the current token. If the current token doesn't have an * associated value, the return is undefined. * @return the value of the current token. */ public String getValue() { return tokenValue; } /** * Gets the discipline associated with the current token. If the current token * doesn't have an associated value, the return is undefined. * @return the discipline of the current token. */ public int getDisciplineLevel() { return discipLevel; } } /** * Tokenizes display strings containing courses and disciplines. The workings of this * class are nearly identical to DatabaseReqStrTokenizer and are not described. */ class CourseDisplayReqStrTokenizer { private String str; private StringTokenizer k; private int type; private String tokenValue; private int discipLevel; public CourseDisplayReqStrTokenizer(String in) { str = in; k = new StringTokenizer(in, " "); } private static int getTokenType(String tk) { if(tk.equalsIgnoreCase("or")) return TokenMap.OR; else if(tk.equalsIgnoreCase("xor")) return TokenMap.XOR; else if(tk.equalsIgnoreCase("and")) return TokenMap.AND; else if(tk.equalsIgnoreCase("(")) return TokenMap.LPAREN; else if(tk.equalsIgnoreCase(")")) return TokenMap.RPAREN; else if(tk.equalsIgnoreCase("all")) return TokenMap.DISIPL; else if(tk.equalsIgnoreCase("upper")) return TokenMap.DISIPL; else if(tk.equalsIgnoreCase("lower")) return TokenMap.DISIPL; else if(tk.equalsIgnoreCase("liberal_arts")) return TokenMap.LARTS; else return TokenMap.COURSE; } private boolean hasMore() { return k.hasMoreTokens(); } public boolean next() { if(!hasMore()) return false; String tk = k.nextToken(); type = getTokenType(tk); if(type == TokenMap.COURSE) { tokenValue = tk; if(!hasMore()) throw new RequirementParserException( "Requirement String " + str + " was invalid."); tokenValue += " " + k.nextToken(); return true; } else if(type == TokenMap.DISIPL) { if(tk.equalsIgnoreCase("all")) discipLevel = 0; else if(tk.equalsIgnoreCase("upper")) discipLevel = 2; else if(tk.equalsIgnoreCase("lower")) discipLevel = 1; if(!hasMore()) throw new RequirementParserException( "Requirement String " + str + " was invalid."); tokenValue = k.nextToken(); return true; } return true; } public int getType() { return type; } public String getValue() { return tokenValue; } public int getDisciplineLevel() { return discipLevel; } } /** * Tokenizes display strings containing majors. The workings of this * class are nearly identical to DatabaseReqStrTokenizer and are not described. */ class MajorDisplayReqStrTokenizer { private String str; private StringTokenizer k; private int type; private String tokenValue; public MajorDisplayReqStrTokenizer(String in) { str = in; k = new StringTokenizer(in, " "); } private static int getTokenType(String tk) { if(tk.equalsIgnoreCase("or")) return TokenMap.OR; else if(tk.equalsIgnoreCase("xor")) return TokenMap.XOR; else if(tk.equalsIgnoreCase("and")) return TokenMap.AND; else if(tk.equalsIgnoreCase("(")) return TokenMap.LPAREN; else if(tk.equalsIgnoreCase(")")) return TokenMap.RPAREN; else return TokenMap.MAJOR; } private boolean hasMore() { return k.hasMoreTokens(); } public boolean next() { if(!hasMore()) return false; String tk = k.nextToken(); type = getTokenType(tk); if(type == TokenMap.MAJOR) tokenValue = tk; return true; } public int getType() { return type; } public String getValue() { return tokenValue; } } /** * Checks the syntax of a sequence of tokens, ensuring that what a user has typed in * is indeed valid, so that it can be stored in the database. */ class SyntaxChecker { private static final int PREV = 0; private static final int CURR = 1; private static final int[][] VALID_OR_XOR_AND = { { TokenMap.NUMBER, TokenMap.CARROT, TokenMap.RPAREN }, // tokens allowed prior to this token { TokenMap.NUMBER, TokenMap.CARROT, TokenMap.LPAREN } // tokens allowed after this token }; private static final int[][] VALID_LPAREN = { { TokenMap.AND, TokenMap.OR, TokenMap.XOR, TokenMap.NUMBER, TokenMap.CARROT, TokenMap.LPAREN }, { TokenMap.CARROT, TokenMap.NUMBER, TokenMap.LPAREN } }; private static final int[][] VALID_RPAREN = { { TokenMap.NUMBER, TokenMap.CARROT, TokenMap.RPAREN }, { TokenMap.AND, TokenMap.OR, TokenMap.XOR, TokenMap.NUMBER, TokenMap.CARROT, TokenMap.RPAREN } }; private static final int[][] VALID_NUMBER_CARROT = { { TokenMap.OR, TokenMap.XOR, TokenMap.AND, TokenMap.LPAREN }, { TokenMap.OR, TokenMap.XOR, TokenMap.AND, TokenMap.RPAREN } }; private static final int[][] VALID_LARTS = { {}, {} }; //number of tokens checked into this instance so far private int numChecked; // current level of parentheses nesting (restricted to one currently) private int parenLevel; // the previous token and current token (values from TokenMap class) private int prevToken, currToken; // what to put into the exception string to represent previous and current token if one is thrown private String prevDisp, currDisp; /** * Constructs a new SyntaxChecker instance. */ public SyntaxChecker() { numChecked = 0; parenLevel = 0; } /** * Maps a token type to a 2D array listing what tokens are valid before and after * the token type passed in. * @param token the type of array to return. * @return a 2D array listing the types of tokens allowed surrounding the token passed in. */ private static int[][] getTokenArray(int token) { switch(token) { case TokenMap.OR: case TokenMap.XOR: case TokenMap.AND: return VALID_OR_XOR_AND; case TokenMap.LPAREN: return VALID_LPAREN; case TokenMap.RPAREN: return VALID_RPAREN; case TokenMap.NUMBER: case TokenMap.CARROT: return VALID_NUMBER_CARROT; case TokenMap.LARTS: return VALID_LARTS; } throw new RequirementParserException( "Invalid Token " + token + " in display string."); } /** * Validates that the token passed in is allowed before or after the token corresponding * to the 2D array. * @param token check if this token is allowed before/after the token-array token. * @param which PREV if checking before token, or CURR if checking after the token. * @return true if the token is allowed, false if not. */ private static boolean tokenMatchesArray(int token, int which, int[][] array) { for(int i=0; i < array[which].length; ++i) if(token == array[which][i]) return true; return false; } /** * Ensures that the previous token and the current token are allowed syntactically * to appear next to each other. */ private void checkSyntax() { int[][] arrPrev = getTokenArray(prevToken); int[][] arrCurr = getTokenArray(currToken); if(!tokenMatchesArray(currToken, CURR, arrPrev) || !tokenMatchesArray(prevToken, PREV, arrCurr)) throw new RequirementParserException( "Display string invalid: a '" + prevDisp + "' followed by a '" + currDisp + "' is illegal."); } /** * Checks in a token, makes sure it is valid in context of the rest of the * display string's tokens (so far). * @param token the token to check in. * @param dispStr a string to use for parameter token if the call generates an exception. * Exists to present the user with a more understandable representation than an integer. * @exception RequirementParserException thrown if the display string is syntactically invalid. */ public void checkToken(int token, String dispStr) { switch(numChecked++) { case 0: prevToken = token; prevDisp = dispStr; if(token == TokenMap.OR || token == TokenMap.AND) throw new RequirementParserException( "Display string invalid: '" + prevDisp + "' cannot start the display string."); break; case 1: currToken = token; currDisp = dispStr; checkSyntax(); break; default: prevToken = currToken; currToken = token; prevDisp = currDisp; currDisp = dispStr; checkSyntax(); break; } if(token == TokenMap.LPAREN) parenLevel++; if(token == TokenMap.RPAREN) parenLevel--; if(parenLevel < 0) throw new RequirementParserException( "Display string invalid: mismatched right parenthesis found."); if(parenLevel > 1) { throw new RequirementParserException( "Display string invalid: parenthesis nesting not allowed."); } } /** * Tells the instance you are doing checking in tokens, and that it should * ensure the way the string has ended is valid, meaning parens are closed, * he last token wasn't an operator, and at least one token has been checked. */ public void finishChecking() { if(numChecked == 0) return; else if(numChecked == 1) currToken = prevToken; if(currToken == TokenMap.OR || currToken == TokenMap.XOR || currToken == TokenMap.AND) throw new RequirementParserException( "Display string invalid: '" + currDisp + "' cannot end the display string."); if(parenLevel > 0) throw new RequirementParserException( "Display string invalid: " + parenLevel + " mismatched left parentheses found."); } } /** * Enumeration of token values used throughout the classes of the RequirementParser. */ class TokenMap { public static final int OR = 0; public static final int AND = 1; public static final int LPAREN = 2; public static final int RPAREN = 3; public static final int CARROT = 4; public static final int COURSE = 5; public static final int LARTS = 6; public static final int DISIPL = 7; public static final int INVALID = 8; public static final int NUMBER = 9; public static final int SPACE = 10; public static final int MAJOR = 11; public static final int XOR = 12; } /** * An exception class utilized in the RequirementParser class and * the classes present in its source file. */ class RequirementParserException extends RuntimeException { public RequirementParserException(String msg) { super(msg); } }