diff --git a/.gitignore b/.gitignore index f1920dd..509124e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ hs_err_pid* /.settings/org.eclipse.jdt.core.prefs /.settings/org.eclipse.m2e.core.prefs /target/test-classes +/target/ diff --git a/src/main/java/co/aurasphere/gomorrasql/GomorraSqlInterpreter.java b/src/main/java/co/aurasphere/gomorrasql/GomorraSqlInterpreter.java index 376fbff..f441c97 100644 --- a/src/main/java/co/aurasphere/gomorrasql/GomorraSqlInterpreter.java +++ b/src/main/java/co/aurasphere/gomorrasql/GomorraSqlInterpreter.java @@ -5,7 +5,6 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.List; -import java.util.StringTokenizer; import java.util.stream.Collectors; import co.aurasphere.gomorrasql.model.CaggiaFaException; @@ -42,14 +41,11 @@ public static String toSqlQuery(String gomorraQuery) { private static QueryInfo parseQuery(String query) { AbstractState currentState = new InitialState(); - // TODO: bug: whitespaces inside quotes should be ignored - StringTokenizer tokenizer = new StringTokenizer(query, ", ", true); - while (tokenizer.hasMoreTokens()) { - String nextToken = tokenizer.nextToken().trim(); - if (!nextToken.isEmpty()) { - currentState = currentState.transitionToNextState(nextToken); - } - } + + List result = SQLTokenizer.tokenize(query); + for( String token : result ){ + currentState = currentState.transitionToNextState(token); + } if (!currentState.isFinalState()) { throw new CaggiaFaException("Unexpected end of query"); @@ -113,7 +109,7 @@ private static String buildUpdateQuery(QueryInfo queryInfo) { private static String buildInsertQuery(QueryInfo queryInfo) { StringBuilder query = new StringBuilder("INSERT INTO ").append(queryInfo.getTableName()); if (!queryInfo.getColumnNames().isEmpty()) { - query.append(" ( ").append(queryInfo.getColumnNames().stream().collect(Collectors.joining(", "))) + query.append(" ( ").append(queryInfo.getColumnNames().stream().collect( Collectors.joining(", "))) .append(" )"); } query.append(" VALUES ( ").append(queryInfo.getValues().stream().collect(Collectors.joining(", "))) @@ -125,7 +121,10 @@ private static String buildSelectQuery(QueryInfo queryInfo) { String query = "SELECT "; // Column names - query += queryInfo.getColumnNames().stream().collect(Collectors.joining(", ")); + String columns = queryInfo.getColumnNames().stream().collect(Collectors.joining(", ")); + + // AS trick + query += columns.replace("|", " AS "); // Table name query += " FROM " + queryInfo.getTableName(); diff --git a/src/main/java/co/aurasphere/gomorrasql/Keywords.java b/src/main/java/co/aurasphere/gomorrasql/Keywords.java index 48a552e..15142fc 100644 --- a/src/main/java/co/aurasphere/gomorrasql/Keywords.java +++ b/src/main/java/co/aurasphere/gomorrasql/Keywords.java @@ -11,25 +11,27 @@ */ public class Keywords { - public final static String SELECT_KEYWORD = "ripigliammo"; - public final static String UPDATE_KEYWORD = "rifacimm"; - public final static String[] INSERT_KEYWORDS = { "nzipp", "'ngoppa" }; + public static final String SELECT_KEYWORD = "ripigliammo"; + public static final String UPDATE_KEYWORD = "rifacimm"; + public static final String[] INSERT_KEYWORDS = { "nzipp", "'ngoppa" }; public static final String[] DELETE_KEYWORDS = { "facimm", "na'", "strage" }; public static final String[] JOIN_KEYWORDS = { "pesc", "e", "pesc" }; public static final String[] FROM_KEYWORDS = { "mmiez", "'a" }; - public final static String[] ASTERISK_KEYWORDS = { "tutto", "chillo", "ch'era", "'o", "nuostro" }; - public final static String WHERE_KEYWORD = "arò"; - public final static String[] BEGIN_TRANSACTION_KEYWORDS = { "ua", "uagliò" }; - public final static String[] COMMIT_KEYWORDS = { "iamme", "bello", "ia'" }; - public final static String ROLLBACK_KEYWORD = "sfaccimm"; - public final static String AND_KEYWORD = "e"; - public final static String OR_KEYWORD = "o"; - public final static String NULL_KEYWORD = "nisciun"; - public final static String IS_KEYWORD = "è"; - public final static String VALUES_KEYWORD = "chist"; - public final static String[] IS_NOT_KEYWORDS = { "nun", "è" }; - public final static String SET_KEYWORD = "accunza"; - public final static List WHERE_OPERATORS = Arrays.asList(">", "<", "=", "!=", "<>", ">=", "<=", + public static final String[] ASTERISK_KEYWORDS = { "tutto", "chillo", "ch'era", "'o", "nuostro" }; + public static final String WHERE_KEYWORD = "arò"; + public static final String[] BEGIN_TRANSACTION_KEYWORDS = { "ua", "uagliò" }; + public static final String[] COMMIT_KEYWORDS = { "iamme", "bello", "ia'" }; + public static final String ROLLBACK_KEYWORD = "sfaccimm"; + public static final String AND_KEYWORD = "e"; + public static final String OR_KEYWORD = "o"; + public static final String NULL_KEYWORD = "nisciun"; + public static final String IS_KEYWORD = "è"; + public static final String VALUES_KEYWORD = "chist"; + public static final String[] IS_NOT_KEYWORDS = { "nun", "è" }; + public static final String SET_KEYWORD = "accunza"; + public static final List WHERE_OPERATORS = Arrays.asList(">", "<", "=", "!=", "<>", ">=", "<=", Keywords.IS_KEYWORD, Keywords.IS_NOT_KEYWORDS[0]); - public final static String SET_EQUAL_KEYWORD = "accussì"; + public static final String SET_EQUAL_KEYWORD = "accussì"; + public static final String LIMIT_KEYWORD = "è pccirill ashpiett"; + public static final String AS_KEYWORD = "cumme"; } \ No newline at end of file diff --git a/src/main/java/co/aurasphere/gomorrasql/SQLTokenizer.java b/src/main/java/co/aurasphere/gomorrasql/SQLTokenizer.java new file mode 100644 index 0000000..8d41326 --- /dev/null +++ b/src/main/java/co/aurasphere/gomorrasql/SQLTokenizer.java @@ -0,0 +1,60 @@ +package co.aurasphere.gomorrasql; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Matteo + */ +public final class SQLTokenizer { + + /** + * Private constructor for utility class. + */ + private SQLTokenizer() { + + } + + /** + * Tokenize the input query in single usable tokens. + * + * @param query the query to tokenize + * @return the arrya list of String tokens + */ + static List tokenize(final String query) { + List result = new ArrayList<>(); + + boolean quote = false; + StringBuilder currentToken = new StringBuilder(); + for (int n = 0; n < query.length(); n++) { + char chr = query.charAt(n); + + // Quote check + if (!quote && chr == '"') { + quote = true; + } else if (quote && chr == '"') { + quote = false; + } + + if (quote) { + currentToken.append(chr); + } else if (chr == ' ' || chr == ',') { + if (currentToken.length() > 0) { + result.add(currentToken.toString()); + currentToken = new StringBuilder(); + } + if (chr == ',') { + result.add("" + chr); + } + } else { + currentToken.append(chr); + } + } + if (currentToken.length() > 0) { + result.add(currentToken.toString()); + } + + return result; + } +} diff --git a/src/main/java/co/aurasphere/gomorrasql/model/QueryInfo.java b/src/main/java/co/aurasphere/gomorrasql/model/QueryInfo.java index 939f670..4b2e515 100644 --- a/src/main/java/co/aurasphere/gomorrasql/model/QueryInfo.java +++ b/src/main/java/co/aurasphere/gomorrasql/model/QueryInfo.java @@ -19,15 +19,17 @@ public enum QueryType { private String tableName; - private List columnNames = new ArrayList<>(); + private final List columnNames = new ArrayList<>(); - private List values = new ArrayList<>(); + private final List columnAliases = new ArrayList<>(); - private List whereConditions = new ArrayList<>(); + private final List values = new ArrayList<>(); - private List joinedTables = new ArrayList<>(); + private final List whereConditions = new ArrayList<>(); - private List whereConditionsJoinOperators = new ArrayList<>(); + private final List joinedTables = new ArrayList<>(); + + private final List whereConditionsJoinOperators = new ArrayList<>(); public QueryType getType() { return type; @@ -49,8 +51,13 @@ public List getColumnNames() { return columnNames; } - public void addColumnName(String columnName) { - this.columnNames.add(columnName); + public List getColumnAliases() { + return columnAliases; + } + + public void addColumnName(String columnName) { + this.columnNames.add( columnName ); + this.columnAliases.add( columnName ); } public List getValues() { diff --git a/src/main/java/co/aurasphere/gomorrasql/states/AnyTokenConsumerState.java b/src/main/java/co/aurasphere/gomorrasql/states/AnyTokenConsumerState.java index c3a11ee..a324f8c 100644 --- a/src/main/java/co/aurasphere/gomorrasql/states/AnyTokenConsumerState.java +++ b/src/main/java/co/aurasphere/gomorrasql/states/AnyTokenConsumerState.java @@ -15,8 +15,8 @@ */ public class AnyTokenConsumerState extends AbstractState { - private Consumer tokenConsumer; - private Function transitionFunction; + private final Consumer tokenConsumer; + private final Function transitionFunction; public AnyTokenConsumerState(QueryInfo queryInfo, Consumer tokenConsumer, Function transitionFunction) { diff --git a/src/main/java/co/aurasphere/gomorrasql/states/CommaSeparedValuesWithAliasState.java b/src/main/java/co/aurasphere/gomorrasql/states/CommaSeparedValuesWithAliasState.java new file mode 100644 index 0000000..dcf5a4f --- /dev/null +++ b/src/main/java/co/aurasphere/gomorrasql/states/CommaSeparedValuesWithAliasState.java @@ -0,0 +1,99 @@ +package co.aurasphere.gomorrasql.states; + +import co.aurasphere.gomorrasql.Keywords; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import co.aurasphere.gomorrasql.model.CaggiaFaException; +import co.aurasphere.gomorrasql.model.QueryInfo; + +/** + * State that parses a list of values, separed by a comma. It stops when there's + * no more comma and checks if the token after is the expected one (set in the + * constructor). It can be configured to be a final state. + * + * @author Matteo Baccan + * + */ +public class CommaSeparedValuesWithAliasState extends AbstractState { + + private boolean lastWasComma = false; + private List collector; + private String nextToken; + private Function transitionFunction; + private String expectedToken; + private boolean canBeFinalState = false; + private boolean optionalValues = false; + private boolean lastWasAS = false; + + public CommaSeparedValuesWithAliasState(QueryInfo queryInfo, List collector, String nextToken, String expectedToken, + Function transitionFunction) { + super(queryInfo); + this.collector = collector; + this.nextToken = nextToken; + this.transitionFunction = transitionFunction; + this.expectedToken = expectedToken; + } + + public CommaSeparedValuesWithAliasState(QueryInfo queryInfo, List collector, String nextToken, String expectedToken, + boolean lastWasComma, boolean canBeFinalState, Function transitionFunction) { + this(queryInfo, collector, nextToken, expectedToken, transitionFunction); + + // Used when the first token is not consumed by the previous state. + this.lastWasComma = lastWasComma; + this.canBeFinalState = canBeFinalState; + this.optionalValues = true; + } + + @Override + public AbstractState transitionToNextState(String token) throws CaggiaFaException { + if (token.equals(",")) { + if (lastWasComma) { + // Case ", ," + throw new CaggiaFaException(expectedToken, token); + } else { + // Case "%expectedToken% ," + lastWasComma = true; + return this; + } + } + + // Case ", %expectedToken%" + if (lastWasComma) { + if (optionalValues && token.equalsIgnoreCase(nextToken)) { + return transitionFunction.apply(queryInfo); + } else { + optionalValues = false; + } + collector.add(token); + lastWasComma = false; + return this; + } + + if( token.equalsIgnoreCase(Keywords.AS_KEYWORD) ){ + lastWasAS = true; + return this; + } + + if (lastWasAS) { + int pos = collector.size()-1; + collector.set( pos, collector.get( pos ) + "|" + token ); + lastWasAS = false; + return this; + } + + // Case "%expectedToken% %nextToken%" + if (token.equalsIgnoreCase(nextToken)) { + return transitionFunction.apply(queryInfo); + } + + // Case "%expectedToken% %WRONG_TOKEN%" + throw new CaggiaFaException(Arrays.asList(",", nextToken), token); + } + + @Override + public boolean isFinalState() { + return canBeFinalState; + } +} \ No newline at end of file diff --git a/src/main/java/co/aurasphere/gomorrasql/states/GreedyMatchKeywordState.java b/src/main/java/co/aurasphere/gomorrasql/states/GreedyMatchKeywordState.java index 748813e..915be71 100644 --- a/src/main/java/co/aurasphere/gomorrasql/states/GreedyMatchKeywordState.java +++ b/src/main/java/co/aurasphere/gomorrasql/states/GreedyMatchKeywordState.java @@ -15,8 +15,8 @@ public class GreedyMatchKeywordState extends AbstractState { private int currentIndex = 1; - private String[] keywords; - private Function nextStateTransition; + private final String[] keywords; + private final Function nextStateTransition; public GreedyMatchKeywordState(QueryInfo queryInfo, String[] keywords, Function nextStateTransition) { diff --git a/src/main/java/co/aurasphere/gomorrasql/states/InitialState.java b/src/main/java/co/aurasphere/gomorrasql/states/InitialState.java index 9e4c5f5..e0c69b3 100644 --- a/src/main/java/co/aurasphere/gomorrasql/states/InitialState.java +++ b/src/main/java/co/aurasphere/gomorrasql/states/InitialState.java @@ -47,9 +47,8 @@ public AbstractState transitionToNextState(String token) throws CaggiaFaExceptio queryInfo.setType(QueryType.INSERT); return new GreedyMatchKeywordState(queryInfo, Keywords.INSERT_KEYWORDS, q -> new AnyTokenConsumerState(q, q::setTableName, - q2 -> new CommaSeparedValuesState(q2, q2.getColumnNames(), Keywords.VALUES_KEYWORD, - "%COLUMN_NAME%", true, false, q3 -> new CommaSeparedValuesState(q3, q3.getValues(), - null, "%VALUE%", true, true, FinalState::new)))); + q2 -> new CommaSeparedValuesState(q2, q2.getColumnNames(), Keywords.VALUES_KEYWORD, "%COLUMN_NAME%", true, false, + q3 -> new CommaSeparedValuesState(q3, q3.getValues(), null, "%VALUE%", true, true, FinalState::new)))); } if (token.equalsIgnoreCase(Keywords.COMMIT_KEYWORDS[0])) { queryInfo.setType(QueryType.COMMIT); diff --git a/src/main/java/co/aurasphere/gomorrasql/states/query/SelectColumnsState.java b/src/main/java/co/aurasphere/gomorrasql/states/query/SelectColumnsState.java index 1fc5a56..b4f327c 100644 --- a/src/main/java/co/aurasphere/gomorrasql/states/query/SelectColumnsState.java +++ b/src/main/java/co/aurasphere/gomorrasql/states/query/SelectColumnsState.java @@ -5,7 +5,7 @@ import co.aurasphere.gomorrasql.model.QueryInfo; import co.aurasphere.gomorrasql.states.AbstractState; import co.aurasphere.gomorrasql.states.AnyTokenConsumerState; -import co.aurasphere.gomorrasql.states.CommaSeparedValuesState; +import co.aurasphere.gomorrasql.states.CommaSeparedValuesWithAliasState; import co.aurasphere.gomorrasql.states.GreedyMatchKeywordState; /** @@ -32,8 +32,8 @@ public AbstractState transitionToNextState(String token) throws CaggiaFaExceptio } else { // Token is a column name, we continue until there are none queryInfo.addColumnName(token); - return new CommaSeparedValuesState(queryInfo, queryInfo.getColumnNames(), Keywords.FROM_KEYWORDS[0], - "%COLUMN_NAME%", q -> new GreedyMatchKeywordState(queryInfo, Keywords.FROM_KEYWORDS, + return new CommaSeparedValuesWithAliasState(queryInfo, queryInfo.getColumnNames(), Keywords.FROM_KEYWORDS[0], "%COLUMN_NAME%", + q -> new GreedyMatchKeywordState(queryInfo, Keywords.FROM_KEYWORDS, q2 -> new AnyTokenConsumerState(q2, q2::setTableName, OptionalWhereState::new))); } } diff --git a/src/test/java/co/aurasphere/gomorrasql/TestSelect.java b/src/test/java/co/aurasphere/gomorrasql/TestSelect.java index 8a38f6c..9113221 100644 --- a/src/test/java/co/aurasphere/gomorrasql/TestSelect.java +++ b/src/test/java/co/aurasphere/gomorrasql/TestSelect.java @@ -130,4 +130,19 @@ public void testSelectWhereIsNot() throws SQLException { Assert.assertEquals(4, counter); } + @Test + public void testSelectAs() throws SQLException { + GomorraSqlInterpreter gsi = new GomorraSqlInterpreter(connection); + GomorraSqlQueryResult result = gsi.execute("ripigliammo first_name cumme nome, last_name cumme cognome mmiez 'a user arò address nun è nisciun"); + ResultSet resultSet = result.getResultSet(); + int counter = 1; + String[] names = { "PINCO", "PAOLINO", "FRED" }; + while (resultSet.next()) { + String name = resultSet.getString("nome"); + Assert.assertEquals(names[counter - 1], name); + counter++; + } + Assert.assertEquals(4, counter); + } + } \ No newline at end of file diff --git a/src/test/java/co/aurasphere/gomorrasql/TestTokenizer.java b/src/test/java/co/aurasphere/gomorrasql/TestTokenizer.java new file mode 100644 index 0000000..6e14120 --- /dev/null +++ b/src/test/java/co/aurasphere/gomorrasql/TestTokenizer.java @@ -0,0 +1,103 @@ +package co.aurasphere.gomorrasql; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; + +import org.junit.Test; + +/** + * SQLTokenizer used by GomorraSQL. + * + * @author Matteo Baccan + * + */ +public class TestTokenizer { + + @Test + public void testTokenizer() throws SQLException { + String query = "rifacimm user accunza name accussì \"Pinco\""; + + List expectedResult = new ArrayList<>(); + expectedResult.add("rifacimm"); + expectedResult.add("user"); + expectedResult.add("accunza"); + expectedResult.add("name"); + expectedResult.add("accussì"); + expectedResult.add("\"Pinco\""); + + List result = SQLTokenizer.tokenize(query); + + Assert.assertArrayEquals(expectedResult.toArray(), result.toArray()); + } + + @Test + public void testTokenizerWithSpace() throws SQLException { + String query = "rifacimm user accunza name accussì \"Pinco Pallo\""; + + List expectedResult = new ArrayList<>(); + expectedResult.add("rifacimm"); + expectedResult.add("user"); + expectedResult.add("accunza"); + expectedResult.add("name"); + expectedResult.add("accussì"); + expectedResult.add("\"Pinco Pallo\""); + + List result = SQLTokenizer.tokenize(query); + + Assert.assertArrayEquals(expectedResult.toArray(), result.toArray()); + } + + @Test + public void testTokenizerWithComma() throws SQLException { + String query = "rifacimm user accunza name accussì \"Pinco,Pallo\""; + + List expectedResult = new ArrayList<>(); + expectedResult.add("rifacimm"); + expectedResult.add("user"); + expectedResult.add("accunza"); + expectedResult.add("name"); + expectedResult.add("accussì"); + expectedResult.add("\"Pinco,Pallo\""); + + List result = SQLTokenizer.tokenize(query); + + Assert.assertArrayEquals(expectedResult.toArray(), result.toArray()); + } + + @Test + public void testTokenizerWithCommaAndSpaces() throws SQLException { + String query = " rifacimm user accunza name accussì \"Pinco , Pallo\""; + + List expectedResult = new ArrayList<>(); + expectedResult.add("rifacimm"); + expectedResult.add("user"); + expectedResult.add("accunza"); + expectedResult.add("name"); + expectedResult.add("accussì"); + expectedResult.add("\"Pinco , Pallo\""); + + List result = SQLTokenizer.tokenize(query); + + Assert.assertArrayEquals(expectedResult.toArray(), result.toArray()); + } + + @Test + public void testTokenizerWithoutLastQuote() throws SQLException { + String query = " rifacimm user accunza name accussì \"Pinco , Pallo"; + + List expectedResult = new ArrayList<>(); + expectedResult.add("rifacimm"); + expectedResult.add("user"); + expectedResult.add("accunza"); + expectedResult.add("name"); + expectedResult.add("accussì"); + expectedResult.add("\"Pinco , Pallo"); + + List result = SQLTokenizer.tokenize(query); + + Assert.assertArrayEquals(expectedResult.toArray(), result.toArray()); + } + +}