From bc4f7b8b8f18ccf11a50dfbde30d0a1e8c431a8b Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 12 Sep 2026 21:49:13 +0900 Subject: [PATCH] Share parser execution and preserve cancellation signals --- .../jsqlparser/parser/CCJSqlParserUtil.java | 60 ++++---- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 +- .../parser/ParserExecutionTest.java | 142 ++++++++++++++++++ 3 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index 4cf71e719..f187b7da1 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -14,6 +14,8 @@ import java.io.Reader; import java.util.Stack; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -129,6 +131,9 @@ public static Statement parse(String sql, ExecutorService executorService, LOGGER.info("Trying SIMPLE parsing " + (allowComplex ? "first" : "only")); statement = parseStatement(parser.withAllowComplexParsing(false), executorService); } catch (JSQLParserException ex) { + if (wasExecutionStopped(ex)) { + throw ex; + } LOGGER.info("Nesting Depth" + getNestingDepth(sql)); if (allowComplex && (allowedNestingDepth < 0 || getNestingDepth(sql) <= allowedNestingDepth)) { @@ -407,24 +412,7 @@ public static Expression parseCondExpression(String conditionalExpressionStr, public static Statement parseStatement(CCJSqlParser parser, ExecutorService executorService) throws JSQLParserException { - Statement statement; - Future future = executorService.submit(new Callable() { - @Override - public Statement call() throws ParseException { - return parser.Statement(); - } - }); - try { - statement = future.get(parser.getAsLong(Feature.timeOut), - TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - parser.interrupted = true; - future.cancel(true); - throw new JSQLParserException("Time out occurred.", ex); - } catch (Exception ex) { - throw new JSQLParserException(ex); - } - return statement; + return executeParser(parser, executorService, parser::Statement); } /** @@ -478,6 +466,9 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe try { return parseStatements(parser.withAllowComplexParsing(false), executorService); } catch (JSQLParserException ex) { + if (wasExecutionStopped(ex)) { + throw ex; + } // when fast simple parsing fails, try complex parsing but only if it has a chance to // succeed if (allowComplex @@ -502,24 +493,35 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe */ public static Statements parseStatements(CCJSqlParser parser, ExecutorService executorService) throws JSQLParserException { - Statements statements = null; - Future future = executorService.submit(new Callable() { - @Override - public Statements call() throws ParseException { - return parser.Statements(); - } - }); + return executeParser(parser, executorService, parser::Statements); + } + + private static T executeParser(CCJSqlParser parser, ExecutorService executorService, + Callable operation) throws JSQLParserException { + Future future = executorService.submit(operation); try { - statements = future.get(parser.getAsLong(Feature.timeOut), - TimeUnit.MILLISECONDS); + return future.get(parser.getAsLong(Feature.timeOut), TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + parser.interrupted = true; + future.cancel(true); + Thread.currentThread().interrupt(); + throw new JSQLParserException(ex); } catch (TimeoutException ex) { parser.interrupted = true; future.cancel(true); throw new JSQLParserException("Time out occurred.", ex); - } catch (Exception ex) { + } catch (CancellationException ex) { + parser.interrupted = true; + throw new JSQLParserException(ex); + } catch (ExecutionException ex) { throw new JSQLParserException(ex); } - return statements; + } + + private static boolean wasExecutionStopped(JSQLParserException exception) { + Throwable cause = exception.getCause(); + return cause instanceof InterruptedException || cause instanceof TimeoutException + || cause instanceof CancellationException; } public static void streamStatements(StatementListener listener, InputStream is, String encoding) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 92f99568d..a92cde58d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -100,7 +100,7 @@ public class CCJSqlParser extends AbstractJSqlParser { public final static Logger LOGGER = Logger.getLogger(CCJSqlParser.class.getName()); public int bracketsCounter = 0; public int caseCounter = 0; - public boolean interrupted = false; + public volatile boolean interrupted = false; public CCJSqlParser withConfiguration(FeatureConfiguration configuration) { token_source.configuration = configuration; diff --git a/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java b/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java new file mode 100644 index 000000000..09ad06b04 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java @@ -0,0 +1,142 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.Executors; +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ParserExecutionTest { + private static class QueuedExecutor extends AbstractExecutorService { + Future task; + int submissions; + boolean shutdown; + + @Override + public void execute(Runnable command) { + task = (Future) command; + submissions++; + } + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown(); + return List.of(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return shutdown; + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void cancelsTheSubmittedTaskAndPreservesCallerInterruption(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1"); + Thread.currentThread().interrupt(); + try { + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements(parser, executor); + } else { + CCJSqlParserUtil.parseStatement(parser, executor); + } + }); + assertInstanceOf(InterruptedException.class, error.getCause()); + assertTrue(Thread.currentThread().isInterrupted()); + assertTrue(parser.interrupted); + assertTrue(executor.task.isCancelled()); + assertFalse(executor.isShutdown()); + } finally { + Thread.interrupted(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void doesNotRetryInterruptedConvenienceCalls(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + Thread.currentThread().interrupt(); + try { + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements("SELECT 1", executor, null); + } else { + CCJSqlParserUtil.parse("SELECT 1", executor, null); + } + }); + assertInstanceOf(InterruptedException.class, error.getCause()); + assertEquals(1, executor.submissions); + assertTrue(executor.task.isCancelled()); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void timeoutCancelsOneTaskWithoutRetryingOrInterruptingTheCaller(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements("SELECT 1", executor, + parser -> parser.withTimeOut(0)); + } else { + CCJSqlParserUtil.parse("SELECT 1", executor, parser -> parser.withTimeOut(0)); + } + }); + assertInstanceOf(TimeoutException.class, error.getCause()); + assertEquals(1, executor.submissions); + assertTrue(executor.task.isCancelled()); + assertFalse(Thread.currentThread().isInterrupted()); + assertFalse(executor.isShutdown()); + } + + @Test + void successfulAndInvalidParsingKeepTheCallerExecutorUsable() throws Exception { + var executor = Executors.newSingleThreadExecutor(); + try { + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT FROM", executor, null)); + assertEquals("SELECT 2;\n", + CCJSqlParserUtil.parseStatements("SELECT 2", executor, null).toString()); + assertFalse(executor.isShutdown()); + } finally { + executor.shutdownNow(); + } + } +}