| /* |
| * 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.cassandra.cql3; |
| |
| import java.util.LinkedList; |
| |
| import org.antlr.runtime.BaseRecognizer; |
| import org.antlr.runtime.Parser; |
| import org.antlr.runtime.RecognitionException; |
| import org.antlr.runtime.Token; |
| import org.antlr.runtime.TokenStream; |
| import org.apache.cassandra.exceptions.SyntaxException; |
| |
| /** |
| * <code>ErrorListener</code> that collect and enhance the errors send by the CQL lexer and parser. |
| */ |
| public final class ErrorCollector implements ErrorListener |
| { |
| /** |
| * The offset of the first token of the snippet. |
| */ |
| private static final int FIRST_TOKEN_OFFSET = 10; |
| |
| /** |
| * The offset of the last token of the snippet. |
| */ |
| private static final int LAST_TOKEN_OFFSET = 2; |
| |
| /** |
| * The CQL query. |
| */ |
| private final String query; |
| |
| /** |
| * The error messages. |
| */ |
| private final LinkedList<String> errorMsgs = new LinkedList<>(); |
| |
| /** |
| * Creates a new <code>ErrorCollector</code> instance to collect the syntax errors associated to the specified CQL |
| * query. |
| * |
| * @param query the CQL query that will be parsed |
| */ |
| public ErrorCollector(String query) |
| { |
| this.query = query; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void syntaxError(BaseRecognizer recognizer, String[] tokenNames, RecognitionException e) |
| { |
| String hdr = recognizer.getErrorHeader(e); |
| String msg = recognizer.getErrorMessage(e, tokenNames); |
| |
| StringBuilder builder = new StringBuilder().append(hdr) |
| .append(' ') |
| .append(msg); |
| |
| if (recognizer instanceof Parser) |
| appendQuerySnippet((Parser) recognizer, builder); |
| |
| errorMsgs.add(builder.toString()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void syntaxError(BaseRecognizer recognizer, String errorMsg) |
| { |
| errorMsgs.add(errorMsg); |
| } |
| |
| /** |
| * Throws the first syntax error found by the lexer or the parser if it exists. |
| * |
| * @throws SyntaxException the syntax error. |
| */ |
| public void throwFirstSyntaxError() throws SyntaxException |
| { |
| if (!errorMsgs.isEmpty()) |
| throw new SyntaxException(errorMsgs.getFirst()); |
| } |
| |
| /** |
| * Appends a query snippet to the message to help the user to understand the problem. |
| * |
| * @param parser the parser used to parse the query |
| * @param builder the <code>StringBuilder</code> used to build the error message |
| */ |
| private void appendQuerySnippet(Parser parser, StringBuilder builder) |
| { |
| TokenStream tokenStream = parser.getTokenStream(); |
| int index = tokenStream.index(); |
| int size = tokenStream.size(); |
| |
| Token from = tokenStream.get(getSnippetFirstTokenIndex(index)); |
| Token to = tokenStream.get(getSnippetLastTokenIndex(index, size)); |
| Token offending = tokenStream.get(getOffendingTokenIndex(index, size)); |
| |
| appendSnippet(builder, from, to, offending); |
| } |
| |
| /** |
| * Appends a query snippet to the message to help the user to understand the problem. |
| * |
| * @param from the first token to include within the snippet |
| * @param to the last token to include within the snippet |
| * @param offending the token which is responsible for the error |
| */ |
| final void appendSnippet(StringBuilder builder, |
| Token from, |
| Token to, |
| Token offending) |
| { |
| if (!areTokensValid(from, to, offending)) |
| return; |
| |
| String[] lines = query.split("\n"); |
| |
| boolean includeQueryStart = (from.getLine() == 1) && (from.getCharPositionInLine() == 0); |
| boolean includeQueryEnd = (to.getLine() == lines.length) |
| && (getLastCharPositionInLine(to) == lines[lines.length - 1].length()); |
| |
| builder.append(" ("); |
| |
| if (!includeQueryStart) |
| builder.append("..."); |
| |
| String toLine = lines[lineIndex(to)]; |
| int toEnd = getLastCharPositionInLine(to); |
| lines[lineIndex(to)] = toEnd >= toLine.length() ? toLine : toLine.substring(0, toEnd); |
| lines[lineIndex(offending)] = highlightToken(lines[lineIndex(offending)], offending); |
| lines[lineIndex(from)] = lines[lineIndex(from)].substring(from.getCharPositionInLine()); |
| |
| for (int i = lineIndex(from), m = lineIndex(to); i <= m; i++) |
| builder.append(lines[i]); |
| |
| if (!includeQueryEnd) |
| builder.append("..."); |
| |
| builder.append(")"); |
| } |
| |
| /** |
| * Checks if the specified tokens are valid. |
| * |
| * @param tokens the tokens to check |
| * @return <code>true</code> if all the specified tokens are valid ones, |
| * <code>false</code> otherwise. |
| */ |
| private static boolean areTokensValid(Token... tokens) |
| { |
| for (Token token : tokens) |
| { |
| if (!isTokenValid(token)) |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Checks that the specified token is valid. |
| * |
| * @param token the token to check |
| * @return <code>true</code> if it is considered as valid, <code>false</code> otherwise. |
| */ |
| private static boolean isTokenValid(Token token) |
| { |
| return token.getLine() > 0 && token.getCharPositionInLine() >= 0; |
| } |
| |
| /** |
| * Returns the index of the offending token. <p>In the case where the offending token is an extra |
| * character at the end, the index returned by the <code>TokenStream</code> might be after the last token. |
| * To avoid that problem we need to make sure that the index of the offending token is a valid index |
| * (one for which a token exist).</p> |
| * |
| * @param index the token index returned by the <code>TokenStream</code> |
| * @param size the <code>TokenStream</code> size |
| * @return the valid index of the offending token |
| */ |
| private static int getOffendingTokenIndex(int index, int size) |
| { |
| return Math.min(index, size - 1); |
| } |
| |
| /** |
| * Puts the specified token within square brackets. |
| * |
| * @param line the line containing the token |
| * @param token the token to put within square brackets |
| */ |
| private static String highlightToken(String line, Token token) |
| { |
| String newLine = insertChar(line, getLastCharPositionInLine(token), ']'); |
| return insertChar(newLine, token.getCharPositionInLine(), '['); |
| } |
| |
| /** |
| * Returns the index of the last character relative to the beginning of the line 0..n-1 |
| * |
| * @param token the token |
| * @return the index of the last character relative to the beginning of the line 0..n-1 |
| */ |
| private static int getLastCharPositionInLine(Token token) |
| { |
| return token.getCharPositionInLine() + getLength(token); |
| } |
| |
| /** |
| * Return the token length. |
| * |
| * @param token the token |
| * @return the token length |
| */ |
| private static int getLength(Token token) |
| { |
| return token.getText().length(); |
| } |
| |
| /** |
| * Inserts a character at a given position within a <code>String</code>. |
| * |
| * @param s the <code>String</code> in which the character must be inserted |
| * @param index the position where the character must be inserted |
| * @param c the character to insert |
| * @return the modified <code>String</code> |
| */ |
| private static String insertChar(String s, int index, char c) |
| { |
| return new StringBuilder().append(s.substring(0, index)) |
| .append(c) |
| .append(s.substring(index)) |
| .toString(); |
| } |
| |
| /** |
| * Returns the index of the line number on which this token was matched; index=0..n-1 |
| * |
| * @param token the token |
| * @return the index of the line number on which this token was matched; index=0..n-1 |
| */ |
| private static int lineIndex(Token token) |
| { |
| return token.getLine() - 1; |
| } |
| |
| /** |
| * Returns the index of the last token which is part of the snippet. |
| * |
| * @param index the index of the token causing the error |
| * @param size the total number of tokens |
| * @return the index of the last token which is part of the snippet. |
| */ |
| private static int getSnippetLastTokenIndex(int index, int size) |
| { |
| return Math.min(size - 1, index + LAST_TOKEN_OFFSET); |
| } |
| |
| /** |
| * Returns the index of the first token which is part of the snippet. |
| * |
| * @param index the index of the token causing the error |
| * @return the index of the first token which is part of the snippet. |
| */ |
| private static int getSnippetFirstTokenIndex(int index) |
| { |
| return Math.max(0, index - FIRST_TOKEN_OFFSET); |
| } |
| } |