/* * 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.odbc.escape; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.processors.odbc.OdbcUtils; import java.util.LinkedList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ODBC escape sequence parse. */ public class OdbcEscapeUtils { /** Odbc date regexp pattern: '2016-08-23' */ private static final Pattern DATE_PATTERN = Pattern.compile("^'\\d{4}-\\d{2}-\\d{2}'$"); /** Odbc time regexp pattern: '14:33:44' */ private static final Pattern TIME_PATTERN = Pattern.compile("^'\\d{2}:\\d{2}:\\d{2}'$"); /** Odbc timestamp regexp pattern: '2016-08-23 14:33:44.12345' */ private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^'\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(\\.\\d+)?'$"); /** GUID regexp pattern: '12345678-9abc-def0-1234-123456789abc' */ private static final Pattern GUID_PATTERN = Pattern.compile("^'\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}'$"); /** CONVERT function data type parameter pattern: last parameter, after comma */ private static final Pattern CONVERT_TYPE_PATTERN = Pattern.compile(",\\s*(SQL_[\\w_]+)\\s*(?:\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\))?\\s*\\)\\s*$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); /** * 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).result(); } /** * 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 OdbcEscapeParseResult parse0(String text, int startPos, boolean earlyExit) { StringBuilder res = new StringBuilder(); int curPos = startPos; int plainPos = startPos; int openPos = -1; boolean insideLiteral = false; LinkedList<OdbcEscapeParseResult> nested = null; while (curPos < text.length()) { char curChar = text.charAt(curPos); if (curChar == '\'') /* Escaped quote in odbc is two successive singe quotes. They'll flip flag twice without side-effect. */ insideLiteral = !insideLiteral; else if (!insideLiteral) { 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. OdbcEscapeParseResult nestedRes = parse0(text, curPos, true); if (nested == null) nested = new LinkedList<>(); nested.add(nestedRes); curPos += nestedRes.originalLength() - 1; plainPos = curPos + 1; } } 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 { String parseRes; if (nested == null) // Found sequence without nesting, process it. parseRes = parseEscapeSequence(text, openPos, curPos + 1 - openPos); else { // Special case to process nesting. String res0 = appendNested(text, openPos, curPos + 1, nested); nested = null; parseRes = parseEscapeSequence(res0, 0, res0.length()); } if (earlyExit) return new OdbcEscapeParseResult(startPos, curPos + 1 - startPos, parseRes); else res.append(parseRes); openPos = -1; plainPos = curPos + 1; } } } curPos++; } if (openPos != -1) throw new IgniteException("Malformed escape sequence (closing curly brace missing): " + text); if (insideLiteral) throw new IgniteException("Malformed literal expression (closing quote missing): " + text); if (curPos > plainPos) res.append(text, plainPos, curPos); return new OdbcEscapeParseResult(startPos, curPos - startPos + 1, res.toString()); } /** * Parse escape sequence: {escape_sequence}. * * @param text Text. * @param startPos Start position within text. * @param len Length. * @return Result. */ private static String parseEscapeSequence(String text, int startPos, int len) { assert validSubstring(text, startPos, len); char firstChar = text.charAt(startPos); if (firstChar == '{') { char lastChar = text.charAt(startPos + len - 1); if (lastChar != '}') throw new IgniteException("Failed to parse escape sequence because it is not enclosed: " + substring(text, startPos, len)); OdbcEscapeToken token = parseToken(text, startPos, len); return parseEscapeSequence(text, startPos, len, token); } else { // Nothing to escape, return original string. if (startPos == 0 || text.length() == len) return text; else return substring(text, startPos, len); } } /** * Get escape sequence info. * * @param text Text. * @param startPos Start position. * @return Escape sequence info. */ private static OdbcEscapeToken parseToken(String text, int startPos, int len) { assert validSubstring(text, startPos, len); assert text.charAt(startPos) == '{'; int pos = startPos + 1; while (Character.isWhitespace(text.charAt(pos))) pos++; OdbcEscapeType curTyp = null; boolean empty = false; for (OdbcEscapeType typ : OdbcEscapeType.sortedValues()) { if (text.startsWith(typ.body(), pos)) { if (typ.standard()) pos += typ.body().length(); empty = (startPos + len == pos + 1); if (!empty && typ.standard()) { char charAfter = text.charAt(pos); if (!Character.isWhitespace(charAfter)) throw new IgniteException("Unexpected escape sequence token: " + substring(text, startPos, len)); } curTyp = typ; break; } } if (curTyp == null) throw new IgniteException("Unsupported escape sequence: " + substring(text, startPos, len)); if (empty && !curTyp.allowEmpty()) throw new IgniteException("Escape sequence cannot be empty: " + substring(text, startPos, len)); return new OdbcEscapeToken(curTyp, pos - (startPos + 1)); } /** * Parse standard expression: {TOKEN expression} * * @param text Text. * @param startPos Start position. * @param len Length. * @param token Token. * @return Result. */ private static String parseEscapeSequence(String text, int startPos, int len, OdbcEscapeToken token) { assert validSubstring(text, startPos, len); // Get expression borders. int startPos0 = startPos + 1 /* open brace */ + token.length() /* token. */; int len0 = len - 1 /* open brace */ - token.length() /* token */ - 1 /* close brace */; switch (token.type()) { case SCALAR_FUNCTION: return parseScalarFunctionExpression(text, startPos0, len0); case GUID: { String res = parseExpression(text, startPos0, len0, token.type(), GUID_PATTERN); return "CAST(" + res + " AS UUID)"; } case DATE: return parseExpression(text, startPos0, len0, token.type(), DATE_PATTERN); case TIME: return parseExpression(text, startPos0, len0, token.type(), TIME_PATTERN); case TIMESTAMP: return parseExpression(text, startPos0, len0, token.type(), TIMESTAMP_PATTERN); case OUTER_JOIN: return parseExpression(text, startPos0, len0); case CALL: { String val = parseExpression(text, startPos0, len0); return "CALL " + val; } case ESCAPE: case ESCAPE_WO_TOKEN: return parseLikeEscCharacterExpression(text, startPos0, len0); default: throw new IgniteException("Unsupported escape sequence token [text=" + substring(text, startPos, len) + ", token=" + token.type().body() + ']'); } } /** * Parse simple expression. * * @param text Text. * @param startPos Start position. * @param len Length. * @return Parsed expression. */ private static String parseExpression(String text, int startPos, int len) { return substring(text, startPos, len).trim(); } /** * Parse LIKE escape character expression. * * @param text Text. * @param startPos Start position. * @param len Length. * @return Parsed expression. */ private static String parseLikeEscCharacterExpression(String text, int startPos, int len) { return "ESCAPE " + substring(text, startPos, len).trim(); } /** * Parse expression and validate against ODBC specification with regex pattern. * * @param text Text. * @param startPos Start position. * @param len Length. * @return Parsed expression. */ private static String parseExpression(String text, int startPos, int len, OdbcEscapeType type, Pattern pattern) { String val = parseExpression(text, startPos, len); if (!pattern.matcher(val).matches()) throw new IgniteException("Invalid " + type + " escape sequence: " + substring(text, startPos, len)); return val; } /** * Parse scalar function expression. * * @param text Text. * @param startPos Start position. * @param len Length. * @return Parsed expression. */ private static String parseScalarFunctionExpression(String text, int startPos, int len) { int pos = startPos; int endPos = startPos + len; final String errPrefix = "Malformed scalar function escape sequence."; while ((++pos < endPos) && Character.isWhitespace(text.charAt(pos))); if (pos == endPos) throw new IgniteException(errPrefix + " Expected function name."); int funcNamePos = pos; while ((++pos < endPos) && Character.isAlphabetic(text.charAt(pos))); if (pos == endPos) throw new IgniteException(errPrefix + " Expected function parameter list: " + substring(text, startPos, len)); String funcName = text.substring(funcNamePos, pos); switch (funcName.toUpperCase()) { case "CONVERT": { Matcher matcher = CONVERT_TYPE_PATTERN.matcher(text.substring(startPos, endPos)); if (!matcher.find()) throw new IgniteException(errPrefix + " Invalid arguments :" + substring(text, startPos, len)); return (text.substring(startPos, startPos + matcher.start(1)) + OdbcUtils.getIgniteTypeFromOdbcType(matcher.group(1)) + text.substring(startPos + matcher.end(1), startPos + len)).trim(); } default: return substring(text, startPos, len).trim(); } } /** * 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<OdbcEscapeParseResult> nestedRess) { StringBuilder res = new StringBuilder(); int curPos = startPos; for (OdbcEscapeParseResult nestedRes : nestedRess) { // Append text between current position and replace. res.append(text, curPos, nestedRes.originalStart()); // Append replaced text. res.append(nestedRes.result()); // Advance position. curPos = nestedRes.originalStart() + nestedRes.originalLength(); } // Append remainder. res.append(text, curPos, endPos); return res.toString(); } /** * 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(); } /** * Private constructor. */ private OdbcEscapeUtils() { // No-op. } }