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&eacute;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