From c7bede57515d0bd940ad2f4f8591447c39bdf872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Mantelet?= <gregory.mantelet@astro.unistra.fr> Date: Wed, 21 Aug 2019 19:01:59 +0200 Subject: [PATCH] [ADQL] Support hexadecimal values (as numeric values). --- src/adql/parser/grammar/ADQLGrammarBase.java | 11 +++++- src/adql/parser/grammar/adqlGrammar201.jj | 14 ++++--- src/adql/query/operand/NumericConstant.java | 41 ++++++++++++++++++-- src/adql/translator/JDBCTranslator.java | 9 ++++- test/adql/parser/TestADQLParser.java | 25 ++++++++++++ test/adql/translator/TestJDBCTranslator.java | 20 ++++++++++ 6 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/adql/parser/grammar/ADQLGrammarBase.java b/src/adql/parser/grammar/ADQLGrammarBase.java index 7bf92ed..fafa1d3 100644 --- a/src/adql/parser/grammar/ADQLGrammarBase.java +++ b/src/adql/parser/grammar/ADQLGrammarBase.java @@ -22,6 +22,7 @@ package adql.parser.grammar; import java.io.InputStream; import java.util.Stack; +import adql.parser.ADQLParser.ADQLVersion; import adql.parser.ADQLQueryFactory; import adql.query.ADQLQuery; import adql.query.TextPosition; @@ -106,8 +107,14 @@ public abstract class ADQLGrammarBase implements ADQLGrammar { public final void testRegularIdentifier(final Token token) throws ParseException { if (token == null) throw new ParseException("Impossible to test whether NULL is a valid ADQL regular identifier!"); - else if (!isRegularIdentifier(token.image)) - throw new ParseException("Invalid ADQL regular identifier: \u005c"" + token.image + "\u005c"! If it aims to be a column/table name/alias, you should write it between double quotes.", new TextPosition(token)); + else if (!isRegularIdentifier(token.image)) { + String message = "Invalid ADQL regular identifier: \u005c"" + token.image + "\u005c"!"; + if (getVersion() == ADQLVersion.V2_0 && token.image.matches("0[Xx][0-9a-fA-F]+")) + message += " HINT: hexadecimal values are not supported in ADQL-2.0. You should change the grammar version of the ADQL parser to at least ADQL-2.1."; + else + message += " HINT: If it aims to be a column/table name/alias, you should write it between double quotes."; + throw new ParseException(message, new TextPosition(token)); + } } /* ********************************************************************** diff --git a/src/adql/parser/grammar/adqlGrammar201.jj b/src/adql/parser/grammar/adqlGrammar201.jj index 44a8a72..dfce609 100644 --- a/src/adql/parser/grammar/adqlGrammar201.jj +++ b/src/adql/parser/grammar/adqlGrammar201.jj @@ -396,6 +396,7 @@ TOKEN : { < SCIENTIFIC_NUMBER: (<UNSIGNED_FLOAT>|<UNSIGNED_INTEGER>) "E" (<PLUS>|<MINUS>)? <UNSIGNED_INTEGER> > | < UNSIGNED_FLOAT: (<UNSIGNED_INTEGER> <DOT> (<UNSIGNED_INTEGER>)?) | (<DOT> <UNSIGNED_INTEGER>) > | < UNSIGNED_INTEGER: (<DIGIT>)+ > +| < UNSIGNED_HEXADECIMAL: ("0""x" (<DIGIT> | ["a"-"f","A"-"F"])+) > | < #DIGIT: ["0"-"9"] > } @@ -783,11 +784,13 @@ StringConstant String(): {Token t, start=null; String str=""; StringConstant cst NumericConstant UnsignedNumeric(): {Token t; NumericConstant cst;} { (t=<SCIENTIFIC_NUMBER> | t=<UNSIGNED_FLOAT> - | t=<UNSIGNED_INTEGER>) - { try{ - cst = queryFactory.createNumericConstant(t.image); - cst.setPosition(new TextPosition(t)); - return cst; + | t=<UNSIGNED_INTEGER> + | t=<UNSIGNED_HEXADECIMAL>) + { + try{ + cst = queryFactory.createNumericConstant(t.image); + cst.setPosition(new TextPosition(t)); + return cst; }catch(Exception ex){ throw generateParseException(ex); } @@ -796,6 +799,7 @@ NumericConstant UnsignedNumeric(): {Token t; NumericConstant cst;} { NumericConstant UnsignedFloat(): {Token t; NumericConstant cst;} { (t=<UNSIGNED_INTEGER> + | t=<UNSIGNED_HEXADECIMAL> | t=<UNSIGNED_FLOAT>) { try{ diff --git a/src/adql/query/operand/NumericConstant.java b/src/adql/query/operand/NumericConstant.java index a6ececb..e1b024d 100644 --- a/src/adql/query/operand/NumericConstant.java +++ b/src/adql/query/operand/NumericConstant.java @@ -30,7 +30,7 @@ import adql.query.TextPosition; * A numeric (integer, double, ...) constant. * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (07/2019) + * @version 2.0 (08/2019) */ public class NumericConstant implements ADQLOperand { @@ -117,9 +117,38 @@ public class NumericConstant implements ADQLOperand { return value; } + /** + * Tell whether this numeric constant is written in an hexadecimal form. + * + * @return <code>true</code> if written in hexadecimal, + * <code>false</code> otherwise. + * + * @since 2.0 + */ + public final boolean isHexadecimal() { + return isHexadecimal(value); + } + + /** + * Tell whether the given string is an hexadecimal numeric. + * + * @param val The string to test. + * + * @return <code>true</code> if the given string is an hexadecimal value, + * <code>false</code> otherwise. + * + * @since 2.0 + */ + protected boolean isHexadecimal(final String val) { + return val.matches("0[Xx][0-9a-fA-F]+"); + } + public double getNumericValue() { try { - return Double.parseDouble(value); + if (isHexadecimal()) + return Long.parseLong(value.substring(2), 16); + else + return Double.parseDouble(value); } catch(NumberFormatException nfe) { return Double.NaN; } @@ -175,8 +204,12 @@ public class NumericConstant implements ADQLOperand { * in a Double. */ public void setValue(String value, boolean checkNumeric) throws NumberFormatException { - if (checkNumeric) - Double.parseDouble(value); + if (checkNumeric) { + if (isHexadecimal(value)) + Long.parseLong(value.substring(2), 16); + else + Double.parseDouble(value); + } this.value = value; } diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java index 5fe30bb..4ee0a6a 100644 --- a/src/adql/translator/JDBCTranslator.java +++ b/src/adql/translator/JDBCTranslator.java @@ -729,7 +729,14 @@ public abstract class JDBCTranslator implements ADQLTranslator { @Override public String translate(NumericConstant numConst) throws TranslationException { - return numConst.getValue(); + if (numConst.isHexadecimal()) { + try { + return "" + Long.parseLong(numConst.getValue().substring(2), 16); + } catch(NumberFormatException nfe) { + throw new TranslationException("Impossible to evaluate the given hexadecimal expression: \"" + numConst.getValue() + "\"!", nfe); + } + } else + return numConst.getValue(); } @Override diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java index b17a38c..2e00146 100644 --- a/test/adql/parser/TestADQLParser.java +++ b/test/adql/parser/TestADQLParser.java @@ -51,6 +51,31 @@ public class TestADQLParser { public void tearDown() throws Exception { } + @Test + public void testHexadecimal() { + + // CASE: No hexadecimal in ADQL-2.0 + ADQLParser parser = new ADQLParser(ADQLVersion.V2_0); + try { + parser.parseQuery("SELECT 0xF FROM foo"); + fail("Hexadecimal values should not be allowed with ADQL-2.0!"); + } catch(Exception ex) { + assertEquals(ParseException.class, ex.getClass()); + assertEquals("Invalid ADQL regular identifier: \"0xF\"! HINT: hexadecimal values are not supported in ADQL-2.0. You should change the grammar version of the ADQL parser to at least ADQL-2.1.", ex.getMessage()); + } + + // CASE: Hexadecimal allowed in ADQL-2.1 + parser = new ADQLParser(ADQLVersion.V2_1); + try { + assertEquals("SELECT 0xF\nFROM foo", parser.parseQuery("SELECT 0xF FROM foo").toADQL()); + assertEquals("SELECT 0xF*2\nFROM foo", parser.parseQuery("SELECT 0xF*2 FROM foo").toADQL()); + assertEquals("SELECT -0xF\nFROM foo", parser.parseQuery("SELECT -0xF FROM foo").toADQL()); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected error with valid hexadecimal values! (see console for more details)"); + } + } + @Test public void testOffset() { diff --git a/test/adql/translator/TestJDBCTranslator.java b/test/adql/translator/TestJDBCTranslator.java index 0d11e37..fe035ce 100644 --- a/test/adql/translator/TestJDBCTranslator.java +++ b/test/adql/translator/TestJDBCTranslator.java @@ -64,6 +64,26 @@ public class TestJDBCTranslator { } } + @Test + public void testTranslateHexadecimal() { + JDBCTranslator tr = new AJDBCTranslator(); + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + + try { + + assertEquals("SELECT 15 AS \"0xF\"\nFROM foo", tr.translate(parser.parseQuery("Select 0xF From foo"))); + assertEquals("SELECT 15*2 AS \"MULT\"\nFROM foo", tr.translate(parser.parseQuery("Select 0xF*2 From foo"))); + assertEquals("SELECT -15 AS \"NEG_0xF\"\nFROM foo", tr.translate(parser.parseQuery("Select -0xF From foo"))); + + } catch(ParseException pe) { + pe.printStackTrace(System.err); + fail("Unexpected failed query parsing! (see console for more details)"); + } catch(Exception e) { + e.printStackTrace(System.err); + fail("There should have been no problem to translate a query with hexadecimal values into SQL."); + } + } + @Test public void testTranslateStringConstant() { JDBCTranslator tr = new AJDBCTranslator(); -- GitLab