Skip to content
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

Merged
merged 5 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Collaborator

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.

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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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:

|-- java.lang.RuntimeException: Error 3
    at org.opensearch.dataprepper.expression.ParseTreeCompositeExceptionTest.testMultipleExceptionsPrinted(ParseTreeCompositeExceptionTest.java:41)
	at org.opensearch.dataprepper.expression.ParseTreeCompositeException.getCause(ParseTreeCompositeException.java:69)
	at org.gradle.internal.serialize.ExceptionPlaceholder.extractCauses(ExceptionPlaceholder.java:302)
	at org.gradle.internal.serialize.ExceptionPlaceholder.<init>(ExceptionPlaceholder.java:96)
	at org.gradle.internal.serialize.TopLevelExceptionPlaceholder.<init>(TopLevelExceptionPlaceholder.java:27)
	at org.gradle.internal.serialize.ExceptionReplacingObjectOutputStream.doReplaceObject(ExceptionReplacingObjectOutputStream.java:67)
	at org.gradle.internal.serialize.ExceptionReplacingObjectOutputStream$1.transform(ExceptionReplacingObjectOutputStream.java:31)
	at org.gradle.internal.serialize.ExceptionReplacingObjectOutputStream.replaceObject(ExceptionReplacingObjectOutputStream.java:62)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1153)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:353)
	at org.gradle.internal.serialize.Message.send(Message.java:36)
	at org.gradle.internal.serialize.BaseSerializerFactory$ThrowableSerializer.write(BaseSerializerFactory.java:321)
	at org.gradle.internal.serialize.BaseSerializerFactory$ThrowableSerializer.write(BaseSerializerFactory.java:313)
	at org.gradle.internal.remote.internal.hub.DefaultMethodArgsSerializer$ArraySerializer.write(DefaultMethodArgsSerializer.java:80)
	at org.gradle.internal.remote.internal.hub.DefaultMethodArgsSerializer$ArraySerializer.write(DefaultMethodArgsSerializer.java:61)
	at org.gradle.internal.remote.internal.hub.MethodInvocationSerializer$MethodInvocationWriter.writeArgs(MethodInvocationSerializer.java:78)
	at org.gradle.internal.remote.internal.hub.MethodInvocationSerializer$MethodInvocationWriter.write(MethodInvocationSerializer.java:74)
	at org.gradle.internal.remote.internal.hub.MethodInvocationSerializer$MethodInvocationWriter.write(MethodInvocationSerializer.java:58)
	at org.gradle.internal.serialize.kryo.TypeSafeSerializer$2.write(TypeSafeSerializer.java:47)
	at org.gradle.internal.remote.internal.hub.InterHubMessageSerializer$MessageWriter.write(InterHubMessageSerializer.java:104)
	at org.gradle.internal.remote.internal.hub.InterHubMessageSerializer$MessageWriter.write(InterHubMessageSerializer.java:88)
	at org.gradle.internal.remote.internal.inet.SocketConnection.dispatch(SocketConnection.java:122)
	at org.gradle.internal.remote.internal.hub.MessageHub$ConnectionDispatch.run(MessageHub.java:328)
	... 6 more

With override:

|-- java.lang.RuntimeException: Error 3
    at org.opensearch.dataprepper.expression.ParseTreeCompositeExceptionTest.testMultipleExceptionsPrinted(ParseTreeCompositeExceptionTest.java:41)

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataPrepperExpressionParser is needed to access DataPrepperExpressionParser.expression()


@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
Expand Up @@ -4,9 +4,9 @@ interface Parser<T> {

/**
* @since 1.3
* Parse a statement String to an object that can be evaluated by an {@link Evaluator}
* @param statement String to be parsed
* @return Object representing a parsed statement
* Parse a expression String to an object that can be evaluated by an {@link Evaluator}
* @param expression String to be parsed
* @return Object representing a parsed expression
*/
T parse(final String statement);
T parse(final String expression) throws CompositeException;
}
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a comment saying that resetErrors needs to be called on the parsing method?

Copy link
Member Author

Choose a reason for hiding this comment

The 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!"));
}
}
Loading