Alternate implementation.
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/cc8f1e92 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/cc8f1e92 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/cc8f1e92 Branch: refs/heads/ignite-3716 Commit: cc8f1e92fffafc532ac547f3fdf57676ad1bebf5 Parents: 902b45b Author: vozerov-gridgain <[email protected]> Authored: Tue Aug 23 11:58:44 2016 +0300 Committer: vozerov-gridgain <[email protected]> Committed: Tue Aug 23 11:58:44 2016 +0300 ---------------------------------------------------------------------- .../processors/odbc/escape/OdbcParser.java | 289 +++++++++++++++++++ .../OdbcScalarFunctionEscapeSequenceTest.java | 82 ++++++ 2 files changed, 371 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/cc8f1e92/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/escape/OdbcParser.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/escape/OdbcParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/escape/OdbcParser.java new file mode 100644 index 0000000..3d5eff61 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/escape/OdbcParser.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.odbc.escape; + +import org.apache.ignite.IgniteException; + +import java.util.LinkedList; + +/** + * ODBC escape sequence parse. + */ +public class OdbcParser { + /** + * Parse escape sequence. + * + * @param text Original text. + * @return Result. + */ + public static String parse(String text) { + if (text == null) + throw new IgniteException("Text cannot be null."); + + return parse0(text.trim(), 0, false).res; + } + + /** + * Internal parse routine. + * + * @param text Text. + * @param startPos Start position. + * @param earlyExit When set to {@code true} we must return as soon as single expression is parsed. + * @return Parse result. + */ + private static ParseResult parse0(String text, int startPos, boolean earlyExit) { + StringBuilder res = new StringBuilder(); + + int curPos = startPos; + + int plainPos = startPos; + int openPos = -1; + + LinkedList<ParseResult> nested = null; + + while (curPos < text.length()) { + char curChar = text.charAt(curPos); + + if (curChar == '{') { + if (openPos == -1) { + // Top-level opening brace. Append previous portion and remember current position. + res.append(text, plainPos, curPos); + + openPos = curPos; + } + else { + // Nested opening brace -> perform recursion. + ParseResult nestedRes = parse0(text, curPos, true); + + if (nested == null) + nested = new LinkedList<>(); + + nested.add(nestedRes); + + curPos += nestedRes.originalLen; + + plainPos = curPos; + } + } + else if (curChar == '}') { + if (openPos == -1) + // Close without open -> exception. + throw new IgniteException("Malformed escape sequence " + + "(closing curly brace without opening curly brace): " + text); + else { + if (nested == null) { + // Found sequence without nesting, process it. + String parseRes = parseExpression(text, openPos, curPos - openPos); + + if (earlyExit) + return new ParseResult(startPos, curPos - startPos + 1, parseRes); + else + res.append(parseRes); + } + else { + // Process nesting. + String res0 = appendNested(text, openPos, curPos + 1, nested); + + String parseRes = parse0(res0, 0, true).res; + + if (earlyExit) + return new ParseResult(startPos, curPos - startPos + 1, parseRes); + else + res.append(parseRes); + } + + openPos = -1; + + plainPos = curPos + 1; + } + } + + curPos++; + } + + if (curPos > plainPos) + res.append(text, plainPos, curPos); + + return new ParseResult(startPos, curPos - startPos + 1, res.toString()); + } + + /** + * Append nested results. + * + * @param text Original text. + * @param startPos Start position. + * @param endPos End position. + * @param nestedRess Nested results. + * @return Result. + */ + private static String appendNested(String text, int startPos, int endPos, LinkedList<ParseResult> nestedRess) { + StringBuilder res = new StringBuilder(); + + int curPos = startPos; + + for (ParseResult nestedRes : nestedRess) { + // Append text between current position and replace. + res.append(text, curPos, nestedRes.originalStart); + + // Append replaced text. + res.append(nestedRes.res); + + // Advance position. + curPos = nestedRes.originalStart + nestedRes.originalLen; + } + + // Append remainder. + res.append(text, curPos, endPos); + + return res.toString(); + } + + /** + * Parse concrete expression. + * + * @param text Text. + * @param startPos Start position within text. + * @param len Length. + * @return Result. + */ + private static String parseExpression(String text, int startPos, int len) { + assert validSubstring(text, startPos, len); + + char firstChar = text.charAt(startPos); + + if (firstChar == '{') { + char lastChar = text.charAt(startPos + len); + + if (lastChar != '}') + throw new IgniteException("Failed to parse escape sequence because it is not enclosed: " + + substring(text, startPos, len)); + + SequenceType typ = sequenceType(text, startPos, len); + + switch (typ) { + case FN: + return parseScalarExpression(text, startPos, len); + + default: { + assert false : "Unknown expression type: " + typ; + + return null; + } + } + } + else { + // Nothing to escape, return original string. + if (startPos == 0 || text.length() == len) + return text; + else + return text.substring(startPos, startPos + len); + } + } + + /** + * Perform "substring" using start position and length. + * + * @param text Text. + * @param startPos Start position. + * @param len Length. + * @return Substring. + */ + private static String substring(String text, int startPos, int len) { + assert validSubstring(text, startPos, len); + + return text.substring(startPos, startPos + len); + } + + /** + * Check whether substring is valid. + * + * @param text Substring. + * @param startPos Start position. + * @param len Length. + * @return {@code True} if valid. + */ + private static boolean validSubstring(String text, int startPos, int len) { + return text != null && startPos + len <= text.length(); + } + + /** + * Parse concrete expression. + * + * @param text Text. + * @param startPos Start position. + * @param len Length. + * @return Parsed expression. + */ + private static String parseScalarExpression(String text, int startPos, int len) { + assert validSubstring(text, startPos, len); + + return substring(text, startPos + 3, len - 3).trim(); + } + + /** + * Get escape sequence type. + * + * @param text Text. + * @param startPos Start position. + * @return Escape sequence type. + */ + private static SequenceType sequenceType(String text, int startPos, int len) { + assert validSubstring(text, startPos, len); + assert text.charAt(startPos) == '{'; + + if (text.startsWith("fn", startPos + 1)) + return SequenceType.FN; + + throw new IgniteException("Unsupported escape sequence: " + text.substring(startPos, startPos + len)); + } + + /** + * Escape sequence type. + */ + private enum SequenceType { + /** Scalar function. */ + FN + } + + /** + * Parse result. + */ + private static class ParseResult { + /** Original start position. */ + private final int originalStart; + + /** Original length. */ + private final int originalLen; + + /** Resulting text. */ + private final String res; + + /** + * Constructor. + * + * @param originalStart Original start position. + * @param originalLen Original length. + * @param res Resulting text. + */ + public ParseResult(int originalStart, int originalLen, String res) { + this.originalStart = originalStart; + this.originalLen = originalLen; + this.res = res; + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/cc8f1e92/modules/core/src/test/java/org/apache/ignite/internal/processors/odbc/OdbcScalarFunctionEscapeSequenceTest.java ---------------------------------------------------------------------- diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/odbc/OdbcScalarFunctionEscapeSequenceTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/odbc/OdbcScalarFunctionEscapeSequenceTest.java index bab9feb..4aa62c4 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/processors/odbc/OdbcScalarFunctionEscapeSequenceTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/odbc/OdbcScalarFunctionEscapeSequenceTest.java @@ -20,6 +20,7 @@ package org.apache.ignite.internal.processors.odbc; import java.util.concurrent.Callable; import junit.framework.TestCase; import org.apache.ignite.internal.processors.odbc.escape.OdbcEscapeSequenceUtils; +import org.apache.ignite.internal.processors.odbc.escape.OdbcParser; import org.apache.ignite.testframework.GridTestUtils; /** */ @@ -106,4 +107,85 @@ public class OdbcScalarFunctionEscapeSequenceTest extends TestCase { } }, IllegalStateException.class, null); } + + /** */ + public void testTrivial2() throws Exception { + String sqlQry = "{fn test()}"; + String expRes = "test()"; + + String actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + + sqlQry = "select * from table;"; + expRes = "select * from table;"; + + actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + } + + /** */ + public void testSimpleFunction2() throws Exception { + String sqlQry = "select {fn func(field1)} {fn func(field2)} from table;"; + String expRes = "select func(field1) func(field2) from table;"; + + String actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + + sqlQry = "select {fn func(field1)} {fn func(field2)}"; + expRes = "select func(field1) func(field2)"; + + actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + } + + /** */ + public void testNestedFunction2() throws Exception { + String sqlQry = "select {fn func1(field1, {fn func2(field2)}, field3)} from SomeTable;"; + String expRes = "select func1(field1, func2(field2), field3) from SomeTable;"; + + String actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + + sqlQry = "select {fn func1(field1, {fn func2(field2)}, field3)}"; + expRes = "select func1(field1, func2(field2), field3)"; + + actualRes = OdbcParser.parse(sqlQry); + + assertEquals("Scalar function escape sequence parsing fails", expRes, actualRes); + } + + /** */ + public void testFailedOnNotClosedSequence2() throws Exception { + GridTestUtils.assertThrows(null, new Callable<Void>() { + @Override public Void call() throws Exception { + String sqlQry = "select {fn func1(field1, {fn func2(field2), field3)} from SomeTable;"; + + OdbcParser.parse(sqlQry); + + fail("Scalar function escape sequence parsing should failed"); + + return null; + } + }, IllegalStateException.class, null); + } + + /** */ + public void testFailedOnClosingNotOpenedSequence2() throws Exception { + GridTestUtils.assertThrows(null, new Callable<Void>() { + @Override public Void call() throws Exception { + String sqlQry = "select {fn func1(field1, func2(field2)}, field3)} from SomeTable;"; + + OdbcParser.parse(sqlQry); + + fail("Scalar function escape sequence parsing should failed"); + + return null; + } + }, IllegalStateException.class, null); + } }
