From 74000130e85078801a6ae25bdf211c7ca42c5f6c Mon Sep 17 00:00:00 2001 From: manticore-projects Date: Wed, 20 Jul 2022 03:48:49 +0700 Subject: [PATCH] Configurable Parser Timeout via Feature (#1592) * Configurable Parser Timeout via Feature Fixes #1582 Implement Parser Timeout Feature, e. g. `CCJSqlParserUtil.parse(sqlStr, parser -> parser.withTimeOut(60000));` Add a special test failing after a long time only, to test TimeOut vs. Parser Exception * Appease Codacy * Appease Codacy Co-authored-by: Tobias --- README.md | 1 + .../jsqlparser/parser/AbstractJSqlParser.java | 13 ++++ .../jsqlparser/parser/CCJSqlParserUtil.java | 6 +- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../parser/feature/FeatureConfiguration.java | 6 +- .../parser/CCJSqlParserUtilTest.java | 59 +++++++++++++++++++ 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db50e7a00..866660159 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Additionally, we have fixed many errors and improved the code quality and the te * support table option **character set** and **index** options * support Postgresql optional **TABLE** in **TRUNCATE** * support for `ANALYZE mytable` +* Implement Parser Timeout Feature, e. g. `CCJSqlParserUtil.parse(sqlStr, parser -> parser.withTimeOut(60000));` * extended support Postgres' `Extract( field FROM source)` where `field` is a String instead of a Keyword diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 2f54f8d4a..75cff8d2b 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -32,12 +32,21 @@ public P withAllowComplexParsing(boolean allowComplexParsing) { public P withUnsupportedStatements(boolean allowUnsupportedStatements) { return withFeature(Feature.allowUnsupportedStatements, allowUnsupportedStatements); } + + public P withTimeOut(int timeOutMillSeconds) { + return withFeature(Feature.timeOut, timeOutMillSeconds); + } public P withFeature(Feature f, boolean enabled) { getConfiguration().setValue(f, enabled); return me(); } + public P withFeature(Feature f, int value) { + getConfiguration().setValue(f, value); + return me(); + } + public abstract FeatureConfiguration getConfiguration(); public abstract P me(); @@ -46,6 +55,10 @@ public boolean getAsBoolean(Feature f) { return getConfiguration().getAsBoolean(f); } + public Integer getAsInteger(Feature f) { + return getConfiguration().getAsInteger(f); + } + public void setErrorRecovery(boolean errorRecovery) { this.errorRecovery = errorRecovery; } diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index b21634511..16b703576 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.Statements; @@ -33,7 +34,6 @@ @SuppressWarnings("PMD.CyclomaticComplexity") public final class CCJSqlParserUtil { public final static int ALLOWED_NESTING_DEPTH = 10; - public static final int PARSER_TIMEOUT = 6000; private CCJSqlParserUtil() { } @@ -255,7 +255,7 @@ public Statement call() throws Exception { }); executorService.shutdown(); - statement = future.get(PARSER_TIMEOUT, TimeUnit.MILLISECONDS); + statement = future.get( parser.getConfiguration().getAsInteger(Feature.timeOut), TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { parser.interrupted = true; throw new JSQLParserException("Time out occurred.", ex); @@ -319,7 +319,7 @@ public Statements call() throws Exception { }); executorService.shutdown(); - statements = future.get(PARSER_TIMEOUT, TimeUnit.MILLISECONDS); + statements = future.get( parser.getConfiguration().getAsInteger(Feature.timeOut) , TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { parser.interrupted = true; throw new JSQLParserException("Time out occurred.", ex); diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 4b9cce979..1193a1f01 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -741,6 +741,8 @@ public enum Feature { * needs to be switched off, when VALIDATING statements or parsing blocks */ allowUnsupportedStatements(false), + + timeOut( 6000) ; private Object value; diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java index 117194a21..a8aceb5ce 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java @@ -58,7 +58,11 @@ public Object getValue(Feature feature) { } public boolean getAsBoolean(Feature f) { - return Boolean.valueOf(String.valueOf(getValue(f))); + return Boolean.parseBoolean(String.valueOf(getValue(f))); + } + + public Integer getAsInteger(Feature f) { + return Integer.valueOf(String.valueOf(getValue(f))); } public String getAsString(Feature f) { diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 6cdf422f2..c46cac5ea 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -14,6 +14,8 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; + import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; @@ -29,6 +31,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; public class CCJSqlParserUtilTest { @@ -270,4 +273,60 @@ public void testCondExpressionIssue1482_2() throws JSQLParserException { Expression expr = CCJSqlParserUtil.parseCondExpression("test_table_enum.f1_enum IN ('TEST2'::test.\"test_enum\")", false); assertEquals("test_table_enum.f1_enum IN ('TEST2'::test.\"test_enum\")", expr.toString()); } + + @Test + public void testTimeOutIssue1582() throws InterruptedException { + // This statement is INVALID on purpose + // There are crafted INTO keywords in order to make it fail but only after a long time (40 seconds plus) + + String sqlStr = "" + + "select\n" + + " t0.operatienr\n" + + " , case\n" + + " when\n" + + " case when (t0.vc_begintijd_operatie is null or lpad((extract('hours' into t0.vc_begintijd_operatie::timestamp))::text,2,'0') ||':'|| lpad(extract('minutes' from t0.vc_begintijd_operatie::timestamp)::text,2,'0') = '00:00') then null\n" + + " else (greatest(((extract('hours' into (t0.vc_eindtijd_operatie::timestamp-t0.vc_begintijd_operatie::timestamp))*60 + extract('minutes' from (t0.vc_eindtijd_operatie::timestamp-t0.vc_begintijd_operatie::timestamp)))/60)::numeric(12,2),0))*60\n" + + " end = 0 then null\n" + + " else '25. Meer dan 4 uur'\n" + + " end \n" + + " as snijtijd_interval"; + + // With DEFAULT TIMEOUT 6 Seconds, we expect the statement to timeout normally + // A TimeoutException wrapped into a Parser Exception should be thrown + + assertThrows(TimeoutException.class, new Executable() { + @Override + public void execute() throws Throwable { + try { + CCJSqlParserUtil.parse(sqlStr); + } catch (JSQLParserException ex) { + Throwable cause = ((JSQLParserException) ex).getCause(); + if (cause!=null) { + throw cause; + } else { + throw ex; + } + } + } + }); + + // With custom TIMEOUT 60 Seconds, we expect the statement to not timeout but to fail instead + // No TimeoutException wrapped into a Parser Exception must be thrown + // Instead we expect a Parser Exception only + assertThrows(JSQLParserException.class, new Executable() { + @Override + public void execute() throws Throwable { + try { + CCJSqlParserUtil.parse(sqlStr, parser -> parser.withTimeOut(60000)); + } catch (JSQLParserException ex) { + Throwable cause = ((JSQLParserException) ex).getCause(); + if (cause instanceof TimeoutException) { + throw cause; + } else { + throw ex; + } + } + } + }); + } }