-
Notifications
You must be signed in to change notification settings - Fork 217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added ParseTreeParser #1090
Added ParseTreeParser #1090
Changes from all commits
eddcaa4
5a9f8b3
9611926
30bde1e
7192f19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.expression; | ||
|
||
import javax.annotation.Nullable; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* @since 1.3 | ||
* <p> | ||
* Exception thrown by {@link ParseTreeParser} if ANTLR parse emits error events. | ||
*/ | ||
public class CompositeException extends RuntimeException { | ||
private static final String SEPARATOR = System.getProperty("line.separator"); | ||
|
||
private final Set<Throwable> exceptions; | ||
|
||
public CompositeException(final List<Throwable> exceptions) { | ||
if (exceptions.isEmpty()) { | ||
throw new IllegalArgumentException("exceptions is empty"); | ||
} | ||
|
||
this.exceptions = exceptions.stream() | ||
.map(this::mapNullToNullPointer) | ||
.collect(Collectors.toSet()); | ||
} | ||
|
||
private Throwable mapNullToNullPointer(@Nullable final Throwable throwable) { | ||
if (throwable == null) { | ||
return new NullPointerException("Throwable was null!"); | ||
} | ||
else { | ||
return throwable; | ||
} | ||
} | ||
|
||
private String throwableListToString(final Set<Throwable> exceptions) { | ||
final StringBuilder aggregateMessage = new StringBuilder(); | ||
aggregateMessage.append("Multiple exceptions (") | ||
.append(exceptions.size()) | ||
.append(")") | ||
.append(SEPARATOR); | ||
|
||
for (final Throwable inner : exceptions) { | ||
aggregateMessage.append("|-- "); | ||
aggregateMessage.append(inner.getClass().getCanonicalName()).append(": "); | ||
aggregateMessage.append(inner.getMessage()); | ||
|
||
final StackTraceElement[] stackTrace = inner.getStackTrace(); | ||
if (stackTrace.length > 0) { | ||
aggregateMessage.append(SEPARATOR) | ||
.append(" at ") | ||
.append(stackTrace[0]) | ||
.append(SEPARATOR); | ||
} | ||
} | ||
return aggregateMessage.toString().trim(); | ||
} | ||
|
||
@Override | ||
public synchronized Throwable getCause() { | ||
if (exceptions.size() > 1) { | ||
final String message = throwableListToString(exceptions); | ||
return new ExceptionOverview(message); | ||
} | ||
else { | ||
return exceptions.iterator().next(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.opensearch.dataprepper.expression; | ||
|
||
final class ExceptionOverview extends RuntimeException { | ||
|
||
ExceptionOverview(final String message) { | ||
super(message); | ||
} | ||
|
||
@Override | ||
public synchronized Throwable fillInStackTrace() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason we need to override this method? I did not find it called anywhere in the PR but it maybe under the hood. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overriding the default method reduces the stack-trace size on each cause exception to a single line. Without override:
With override:
|
||
return this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.expression; | ||
|
||
import org.antlr.v4.runtime.CharStreams; | ||
import org.antlr.v4.runtime.CommonTokenStream; | ||
import org.antlr.v4.runtime.IntStream; | ||
import org.antlr.v4.runtime.Lexer; | ||
import org.antlr.v4.runtime.TokenSource; | ||
import org.antlr.v4.runtime.TokenStream; | ||
import org.antlr.v4.runtime.tree.ParseTree; | ||
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser; | ||
|
||
import javax.inject.Inject; | ||
import javax.inject.Named; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
/** | ||
* Handles interaction with ANTLR generated parser and lexer classes and caches results. | ||
*/ | ||
@Named | ||
class ParseTreeParser implements Parser<ParseTree> { | ||
private final Map<String, ParseTree> cache = new HashMap<>(); | ||
private final ParserErrorListener errorListener; | ||
private final Lexer lexer; | ||
private final DataPrepperExpressionParser parser; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is nothing specifically tied to DataPrepperExpressionParser, shall we keep use generic antlr Parser here to make it consistent with Lexer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DataPrepperExpressionParser is needed to access |
||
|
||
@Inject | ||
public ParseTreeParser(final DataPrepperExpressionParser parser, final ParserErrorListener errorListener) { | ||
this.parser = parser; | ||
this.errorListener = errorListener; | ||
this.parser.addErrorListener(errorListener); | ||
|
||
final TokenSource tokenSource = parser.getTokenStream().getTokenSource(); | ||
if (tokenSource instanceof Lexer) { | ||
lexer = (Lexer) tokenSource; | ||
} | ||
else { | ||
throw new ClassCastException("Expected DataPrepperStatementParser token source to be instance of Lexer"); | ||
} | ||
} | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Converts an expression to a parse tree base on grammar rules. {@link ParserErrorListener#resetErrors()} must be called | ||
* before {@link DataPrepperExpressionParser#expression()} to prevent duplicate errors from being reported. | ||
* | ||
* @param expression String to be parsed | ||
* @return ParseTree data structure containing a hierarchy of the tokens found while parsing. | ||
* @throws CompositeException thrown when ANTLR parser creates an exception event | ||
*/ | ||
private ParseTree createParseTree(final String expression) throws CompositeException { | ||
errorListener.resetErrors(); | ||
|
||
final IntStream input = CharStreams.fromString(expression); | ||
lexer.setInputStream(input); | ||
|
||
final TokenStream tokenStream = new CommonTokenStream(lexer); | ||
parser.setTokenStream(tokenStream); | ||
|
||
final ParseTree parseTree = parser.expression(); | ||
|
||
if (errorListener.isErrorFound()) { | ||
throw new CompositeException(errorListener.getExceptions()); | ||
} | ||
else { | ||
return parseTree; | ||
} | ||
} | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Check if cache already has a parse tree available for the given statement. If yes, return from cache otherwise | ||
* parse expression, cache and return result. | ||
* | ||
* @param expression String to be parsed | ||
* @return ParseTree data structure containing a hierarchy of the tokens found while parsing. | ||
* @throws CompositeException thrown when ANTLR parser creates an exception event | ||
*/ | ||
@Override | ||
public ParseTree parse(final String expression) throws CompositeException { | ||
if (cache.containsKey(expression)) { | ||
return cache.get(expression); | ||
} | ||
else { | ||
final ParseTree parseTree = createParseTree(expression); | ||
cache.put(expression, parseTree); | ||
return parseTree; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.expression; | ||
|
||
import org.antlr.v4.runtime.ANTLRErrorListener; | ||
import org.antlr.v4.runtime.Parser; | ||
import org.antlr.v4.runtime.RecognitionException; | ||
import org.antlr.v4.runtime.Recognizer; | ||
import org.antlr.v4.runtime.atn.ATNConfigSet; | ||
import org.antlr.v4.runtime.dfa.DFA; | ||
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser; | ||
|
||
import java.util.ArrayList; | ||
import java.util.BitSet; | ||
import java.util.List; | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Handles any syntaxError events that occur during parsing. All exceptions are tracked and available after parsing is complete. | ||
*/ | ||
class ParserErrorListener implements ANTLRErrorListener { | ||
private final List<Throwable> exceptions = new ArrayList<>(); | ||
|
||
public ParserErrorListener(final DataPrepperExpressionParser parser) { | ||
parser.addErrorListener(this); | ||
} | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Get if any exception events were received | ||
* @return if exceptions received | ||
*/ | ||
public boolean isErrorFound() { | ||
return !exceptions.isEmpty(); | ||
} | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Clears any error events received. Should be called before a new statement is parsed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a comment saying that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a message in the javadoc and added unit test to verify method is called. |
||
*/ | ||
public void resetErrors() { | ||
exceptions.clear(); | ||
} | ||
|
||
/** | ||
* @since 1.3 | ||
* | ||
* Get a list of all exception events received. List is emptied when {@link ParserErrorListener#resetErrors()} | ||
* | ||
* @return list of all exception events received | ||
*/ | ||
public List<Throwable> getExceptions() { | ||
return exceptions; | ||
} | ||
|
||
@Override | ||
public void syntaxError( | ||
final Recognizer<?, ?> recognizer, | ||
final Object offendingSymbol, | ||
final int line, | ||
final int charPositionInLine, | ||
final String msg, | ||
final RecognitionException e | ||
) { | ||
exceptions.add(e); | ||
} | ||
|
||
@Override | ||
public void reportAmbiguity( | ||
final org.antlr.v4.runtime.Parser recognizer, | ||
final DFA dfa, | ||
final int startIndex, | ||
final int stopIndex, | ||
final boolean exact, | ||
final BitSet ambigAlts, | ||
final ATNConfigSet configs | ||
) { | ||
} | ||
|
||
@Override | ||
public void reportAttemptingFullContext( | ||
final org.antlr.v4.runtime.Parser recognizer, | ||
final DFA dfa, | ||
final int startIndex, | ||
final int stopIndex, | ||
final BitSet conflictingAlts, | ||
final ATNConfigSet configs | ||
) { | ||
} | ||
|
||
@Override | ||
public void reportContextSensitivity( | ||
final Parser recognizer, | ||
final DFA dfa, | ||
final int startIndex, | ||
final int stopIndex, | ||
final int prediction, | ||
final ATNConfigSet configs | ||
) { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.expression; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
|
||
import static org.hamcrest.CoreMatchers.containsString; | ||
import static org.hamcrest.CoreMatchers.is; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.Mockito.mock; | ||
|
||
class CompositeExceptionTest { | ||
|
||
@Test | ||
void testNoCausesThrows() { | ||
assertThrows(NullPointerException.class, () -> new CompositeException(null)); | ||
} | ||
|
||
@Test | ||
void testEmptyListThrows() { | ||
assertThrows(IllegalArgumentException.class, () -> new CompositeException(Collections.emptyList())); | ||
} | ||
|
||
@Test | ||
void testGivenSingleExceptionThenExceptionIsCause() { | ||
final RuntimeException mock = mock(RuntimeException.class); | ||
final CompositeException compositeException = new CompositeException(Arrays.asList(mock)); | ||
|
||
assertThat(compositeException.getCause(), is(mock)); | ||
} | ||
|
||
@Test | ||
void testMultipleExceptionsPrinted() throws CompositeException { | ||
final CompositeException compositeException = new CompositeException(Arrays.asList( | ||
new RuntimeException("Error"), | ||
new RuntimeException("Error 2"), | ||
new RuntimeException("Error 3"), | ||
null | ||
)); | ||
|
||
assertThat(compositeException.getCause() instanceof ExceptionOverview, is(true)); | ||
|
||
final String message = compositeException.getCause().getMessage(); | ||
assertThat(message, containsString("Multiple exceptions (4)")); | ||
assertThat(message, containsString("|-- java.lang.RuntimeException: Error 3")); | ||
assertThat(message, containsString("|-- java.lang.RuntimeException: Error 2")); | ||
assertThat(message, containsString("|-- java.lang.RuntimeException: Error")); | ||
assertThat(message, containsString("|-- java.lang.NullPointerException: Throwable was null!")); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest give it a more specific name, e.g. ParseTreeCompositeException.