org.apache.cassandra.cql3.ErrorCollector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cassandra-all Show documentation
Show all versions of cassandra-all Show documentation
Palantir open source project
/*
* 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;
/**
* ErrorListener
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 errorMsgs = new LinkedList<>();
/**
* Creates a new ErrorCollector
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 StringBuilder
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 true
if all the specified tokens are valid ones,
* false
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 true
if it is considered as valid, false
otherwise.
*/
private static boolean isTokenValid(Token token)
{
return token.getLine() > 0 && token.getCharPositionInLine() >= 0;
}
/**
* Returns the index of the offending token. In the case where the offending token is an extra
* character at the end, the index returned by the TokenStream
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).
*
* @param index the token index returned by the TokenStream
* @param size the TokenStream
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 String
.
*
* @param s the String
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 String
*/
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);
}
}