From e3b89775a3db74331d876dc5c74ba64be7a25cfd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9gory=20Mantelet?=
 <gregory.mantelet@astro.unistra.fr>
Date: Tue, 20 Aug 2019 14:29:08 +0200
Subject: [PATCH] [ADQL] Move validation of coord. sys. and STC-s expressions
 from DBChecker to ADQLParser.

---
 src/adql/db/DBChecker.java           | 132 ++++++--
 src/adql/parser/ADQLParser.java      | 486 ++++++++++++++++++++++++---
 test/adql/db/TestDBChecker.java      | 179 ----------
 test/adql/parser/TestADQLParser.java | 214 +++++++++++-
 4 files changed, 739 insertions(+), 272 deletions(-)

diff --git a/src/adql/db/DBChecker.java b/src/adql/db/DBChecker.java
index eda5159..89bb1a2 100644
--- a/src/adql/db/DBChecker.java
+++ b/src/adql/db/DBChecker.java
@@ -77,14 +77,14 @@ import adql.search.SimpleSearchHandler;
  * 		query</li>
  * 	<li>Resolve all unknown functions as supported User Defined Functions
  * 		(UDFs)</li>
- * 	<li>Check whether all used coordinate systems are supported</li>
  * 	<li>Check that types of columns and UDFs match with their context</li>
  * </ol>
  *
  * <p><i><b>IMPORTANT note:</b>
- * 	Since v2.0, the check of supported geometrical functions is performed
- * 	directly in ADQLParser through the notion of Optional Features.
- * 	The declaration of supported geometrical functions must now be done
+ * 	Since v2.0, the check of supported geometric functions, STC-s expressions
+ * 	and coordinate systems are performed automatically in
+ * 	{@link adql.parser.ADQLParser ADQLParser} through the notion of Optional
+ * 	Features. The declaration of supported geometric functions must now be done
  * 	with {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}
  * 	(see also {@link adql.parser.feature.FeatureSet FeatureSet}).
  * </i></p>
@@ -108,11 +108,11 @@ import adql.search.SimpleSearchHandler;
  * 	particularly useful for the translation of the ADQL query to SQL, because
  * 	the ADQL name of columns and tables can be replaced in SQL by their DB name,
  * 	if different. This mapping is done automatically by
- * 	{@link adql.translator.JDBCTranslator}.
+ * 	{@link adql.translator.JDBCTranslator JDBCTranslator}.
  * </i></p>
  *
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (07/2019)
+ * @version 2.0 (08/2019)
  */
 public class DBChecker implements QueryChecker {
 
@@ -148,13 +148,19 @@ public class DBChecker implements QueryChecker {
 	 * 	However, if not, all items of this list must be the only allowed coordinate systems.
 	 * 	So, if the list is empty, none is allowed.
 	 * </p>
-	 * @since 1.3 */
+	 * @since 1.3
+	 * @deprecated Since v2.0, supported coordinate systems must be declared
+	 *             in ADQLParser. */
+	@Deprecated
 	protected String[] allowedCoordSys = null;
 
 	/** <p>A regular expression built using the list of allowed coordinate systems.
 	 * With this regex, it is possible to known whether a coordinate system expression is allowed or not.</p>
 	 * <p>If NULL, all coordinate systems are allowed.</p>
-	 * @since 1.3 */
+	 * @since 1.3
+	 * @deprecated Since v2.0, supported coordinate systems must be declared
+	 *             in ADQLParser. */
+	@Deprecated
 	protected String coordSysRegExp = null;
 
 	/** <p>List of all allowed User Defined Functions (UDFs).</p>
@@ -341,7 +347,13 @@ public class DBChecker implements QueryChecker {
 	 * @return	A sorted array containing all - except NULL and empty strings - items of the given collection.
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since v2.0, this tool function is no longer used. It was
+	 *            	useful only to collect allowed geometries and coordinate
+	 *            	systems....but these are now checked by
+	 *            	{@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected final static String[] specialSort(final Collection<String> items) {
 		// Nothing to do if the array is NULL:
 		if (items == null)
@@ -392,16 +404,18 @@ public class DBChecker implements QueryChecker {
 	/* CHECK METHODS */
 	/* ************* */
 	/**
-	 * <p>Check all the columns, tables and UDFs references inside the given query.</p>
+	 * Check all the columns, tables and UDFs references inside the given query.
 	 *
-	 * <p><i>
-	 * 	<u>Note:</u> This query has already been parsed ; thus it is already syntactically correct.
-	 * 	Only the consistency with the published tables, columns and all the defined UDFs must be checked.
+	 * <p><i><b>Note:</b>
+	 * 	This query has already been parsed ; thus it is already syntactically
+	 * 	correct. Only the consistency with the published tables, columns and all
+	 * 	the defined UDFs must be checked.
 	 * </i></p>
 	 *
 	 * @param query		The query to check.
 	 *
-	 * @throws ParseException	An {@link UnresolvedIdentifiersException} if some tables or columns can not be resolved.
+	 * @throws ParseException	An {@link UnresolvedIdentifiersException} if
+	 *                       	some tables or columns can not be resolved.
 	 *
 	 * @see #check(ADQLQuery, Stack)
 	 */
@@ -411,31 +425,37 @@ public class DBChecker implements QueryChecker {
 	}
 
 	/**
-	 * <p>Process several (semantic) verifications in the given ADQL query.</p>
+	 * Process several (semantic) verifications in the given ADQL query.
 	 *
 	 * <p>Main verifications done in this function:</p>
 	 * <ol>
 	 * 	<li>Existence of DB items (tables and columns)</li>
 	 * 	<li>Semantic verification of sub-queries</li>
-	 * 	<li>Support of every encountered User Defined Functions (UDFs - functions unknown by the syntactic parser)</li>
-	 * 	<li>Support of every encountered geometries (functions, coordinate systems and STC-S expressions)</li>
-	 * 	<li>Consistency of types still unknown (because the syntactic parser could not yet resolve them)</li>
+	 * 	<li>Support of every encountered User Defined Functions (UDFs -
+	 * 		functions unknown by the syntactic parser)</li>
+	 * 	<li>Consistency of types still unknown (because the syntactic parser
+	 * 		could not yet resolve them)</li>
 	 * </ol>
 	 *
 	 * @param query			The query to check.
-	 * @param fathersList	List of all columns available in the father queries and that should be accessed in sub-queries.
-	 *                   	Each item of this stack is a list of columns available in each father-level query.
-	 *                   	<i>Note: this parameter is NULL if this function is called with the root/father query as parameter.</i>
-	 *
-	 * @throws UnresolvedIdentifiersException	An {@link UnresolvedIdentifiersException} if one or several of the above listed tests have detected
-	 *                                       	some semantic errors (i.e. unresolved table, columns, function).
+	 * @param fathersList	List of all columns available in the father queries
+	 *                   	and that should be accessed in sub-queries. Each
+	 *                   	item of this stack is a list of columns available in
+	 *                   	each father-level query. <i><b>Note:</b> this
+	 *                   	parameter is NULL if this function is called with
+	 *                   	the root/father query as parameter.</i>
+	 *
+	 * @throws UnresolvedIdentifiersException	An {@link UnresolvedIdentifiersException}
+	 *                                       	if one or several of the above
+	 *                                       	listed tests have detected some
+	 *                                       	semantic errors (i.e. unresolved
+	 *                                       	table, columns, function).
 	 *
 	 * @since 1.2
 	 *
 	 * @see #checkDBItems(ADQLQuery, Stack, UnresolvedIdentifiersException)
 	 * @see #checkSubQueries(ADQLQuery, Stack, SearchColumnList, UnresolvedIdentifiersException)
 	 * @see #checkUDFs(ADQLQuery, UnresolvedIdentifiersException)
-	 * @see #checkGeometries(ADQLQuery, UnresolvedIdentifiersException)
 	 * @see #checkTypes(ADQLQuery, UnresolvedIdentifiersException)
 	 */
 	protected void check(final ADQLQuery query, final Stack<SearchColumnList> fathersList) throws UnresolvedIdentifiersException {
@@ -448,13 +468,10 @@ public class DBChecker implements QueryChecker {
 		if (allowedUdfs != null)
 			checkUDFs(query, errors);
 
-		// C. Check geometries:
-		checkGeometries(query, errors);
-
-		// D. Check types:
+		// C. Check types:
 		checkTypes(query, errors);
 
-		// E. Check sub-queries:
+		// D. Check sub-queries:
 		checkSubQueries(query, fathersList, availableColumns, errors);
 
 		// Throw all errors, if any:
@@ -1014,7 +1031,16 @@ public class DBChecker implements QueryChecker {
 	 * @see #resolveSTCSExpressions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, validation of the geometric functions is
+	 *            	performed automatically by
+	 *            	{@link adql.parser.ADQLParser ADQLParser}. Geometric
+	 *            	functions are optional features and should be declared as
+	 *            	such in the {@link adql.parser.ADQLParser ADQLParser} if
+	 *            	they are supported (see
+	 *            	{@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}).
 	 */
+	@Deprecated
 	protected void checkGeometries(final ADQLQuery query, final UnresolvedIdentifiersException errors) {
 		BinarySearch<String, String> binSearch = new BinarySearch<String, String>() {
 			@Override
@@ -1041,10 +1067,13 @@ public class DBChecker implements QueryChecker {
 	 * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
-	 * @deprecated	No more used since v2.0. Check of the geometric functions is
-	 *            	now performed directly in ADQLParser. Geometric functions
-	 *            	are optional features and should be declared as such in the
-	 *            	ADQLParser if they are supported (see
+	 *
+	 * @deprecated	Since 2.0, validation of the geometric functions is
+	 *            	performed automatically by
+	 *            	{@link adql.parser.ADQLParser ADQLParser}. Geometric
+	 *            	functions are optional features and should be declared as
+	 *            	such in the {@link adql.parser.ADQLParser ADQLParser} if
+	 *            	they are supported (see
 	 *            	{@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}).
 	 */
 	@Deprecated
@@ -1076,10 +1105,13 @@ public class DBChecker implements QueryChecker {
 	 * @param errors		List of errors to complete in this function each time a geometrical function is not supported.
 	 *
 	 * @since 1.3
-	 * @deprecated	No more used since v2.0. Check of the geometric functions is
-	 *            	now performed directly in ADQLParser. Geometric functions
-	 *            	are optional features and should be declared as such in the
-	 *            	ADQLParser if they are supported (see
+	 *
+	 * @deprecated	Since 2.0, validation of the geometric functions is
+	 *            	performed automatically by
+	 *            	{@link adql.parser.ADQLParser ADQLParser}. Geometric
+	 *            	functions are optional features and should be declared as
+	 *            	such in the {@link adql.parser.ADQLParser ADQLParser} if
+	 *            	they are supported (see
 	 *            	{@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}).
 	 */
 	@Deprecated
@@ -1106,7 +1138,11 @@ public class DBChecker implements QueryChecker {
 	 * @see #checkCoordinateSystem(StringConstant, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, the validation of coordinate systems is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected void resolveCoordinateSystems(final ADQLQuery query, final UnresolvedIdentifiersException errors) {
 		ISearchHandler sHandler = new SearchCoordSysHandler();
 		sHandler.search(query);
@@ -1124,7 +1160,11 @@ public class DBChecker implements QueryChecker {
 	 * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, the validation of coordinate systems is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected void checkCoordinateSystem(final StringConstant adqlCoordSys, final UnresolvedIdentifiersException errors) {
 		String coordSysStr = adqlCoordSys.getValue();
 		try {
@@ -1142,7 +1182,11 @@ public class DBChecker implements QueryChecker {
 	 * @param errors	List of errors to complete in this function each time a coordinate system is not supported.
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, the validation of coordinate systems is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected void checkCoordinateSystem(final CoordSys coordSys, final ADQLOperand operand, final UnresolvedIdentifiersException errors) {
 		if (coordSysRegExp != null && coordSys != null && !coordSys.toFullSTCS().matches(coordSysRegExp)) {
 			StringBuffer buf = new StringBuffer();
@@ -1180,7 +1224,11 @@ public class DBChecker implements QueryChecker {
 	 * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, the validation of STCs expressions is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected void resolveSTCSExpressions(final ADQLQuery query, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) {
 		// Search REGION functions:
 		ISearchHandler sHandler = new SearchRegionHandler();
@@ -1224,7 +1272,11 @@ public class DBChecker implements QueryChecker {
 	 * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException)
 	 *
 	 * @since 1.3
+	 *
+	 * @deprecated	Since 2.0, the validation of REGIONs is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	protected void checkRegion(final Region r, final RegionFunction fct, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) {
 		if (r == null)
 			return;
@@ -1565,7 +1617,10 @@ public class DBChecker implements QueryChecker {
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 1.3 (10/2014)
 	 * @since 1.3
+	 * @deprecated	Since 2.0, the validation of REGIONs is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	private static class SearchCoordSysHandler extends SimpleSearchHandler {
 		@Override
 		protected boolean match(ADQLObject obj) {
@@ -1588,7 +1643,10 @@ public class DBChecker implements QueryChecker {
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 1.3 (10/2014)
 	 * @since 1.3
+	 * @deprecated	Since 2.0, the validation of REGIONs is performed
+	 *            	automatically by {@link adql.parser.ADQLParser ADQLParser}.
 	 */
+	@Deprecated
 	private static class SearchRegionHandler extends SimpleSearchHandler {
 		@Override
 		protected boolean match(ADQLObject obj) {
diff --git a/src/adql/parser/ADQLParser.java b/src/adql/parser/ADQLParser.java
index 8452e3d..dec0fe5 100644
--- a/src/adql/parser/ADQLParser.java
+++ b/src/adql/parser/ADQLParser.java
@@ -23,8 +23,14 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 
 import adql.db.DBChecker;
+import adql.db.STCS;
+import adql.db.STCS.CoordSys;
+import adql.db.STCS.Region;
+import adql.db.STCS.RegionType;
 import adql.db.exception.UnresolvedIdentifiersException;
 import adql.db.exception.UnsupportedFeatureException;
 import adql.parser.feature.FeatureSet;
@@ -36,6 +42,7 @@ import adql.parser.grammar.ADQLGrammar201;
 import adql.parser.grammar.ParseException;
 import adql.parser.grammar.Token;
 import adql.parser.grammar.TokenMgrError;
+import adql.query.ADQLIterator;
 import adql.query.ADQLObject;
 import adql.query.ADQLOrder;
 import adql.query.ADQLQuery;
@@ -44,8 +51,18 @@ import adql.query.ClauseConstraints;
 import adql.query.ClauseSelect;
 import adql.query.from.FromContent;
 import adql.query.operand.ADQLColumn;
+import adql.query.operand.ADQLOperand;
+import adql.query.operand.StringConstant;
+import adql.query.operand.function.geometry.BoxFunction;
+import adql.query.operand.function.geometry.CircleFunction;
 import adql.query.operand.function.geometry.ContainsFunction;
+import adql.query.operand.function.geometry.GeometryFunction;
+import adql.query.operand.function.geometry.PointFunction;
+import adql.query.operand.function.geometry.PolygonFunction;
+import adql.query.operand.function.geometry.RegionFunction;
+import adql.search.ISearchHandler;
 import adql.search.SearchOptionalFeaturesHandler;
+import adql.search.SimpleSearchHandler;
 import adql.translator.PostgreSQLTranslator;
 import adql.translator.TranslationException;
 
@@ -82,11 +99,13 @@ import adql.translator.TranslationException;
  * <p>
  * 	In the above example, the parser runs with the minimal set of options. It
  * 	means that only the default optional language features are available, any
- * 	UDF (even if undeclared) is allowed and no consistency with a list of tables
- * 	and columns is performed. These points can be customized at creation with
+ * 	UDF (even if undeclared) and any coordinate system are allowed and no
+ * 	consistency with a list of tables and columns is performed. These points can
+ * 	be customized at creation with
  * 	{@link #ADQLParser(ADQLVersion, QueryChecker, ADQLQueryFactory, FeatureSet)}
- * 	but also after creation with {@link #setSupportedFeatures(FeatureSet)} and
- * 	{@link #setQueryChecker(QueryChecker)}.
+ * 	but also after creation with {@link #setSupportedFeatures(FeatureSet)},
+ * 	{@link #setQueryChecker(QueryChecker)} and
+ * 	{@link #setAllowedCoordSys(Collection)}.
  * </p>
  *
  * <h3>Runnable class</h3>
@@ -137,15 +156,37 @@ import adql.translator.TranslationException;
  * 		features are supported or not ; all optional features used in the query
  * 		while being declared as un-supported will throw an error at the end of
  * 		the parsing</li>
+ * 	<li>{@link #setAllowedCoordSys(Collection)} to set which coordinate systems
+ * 		are allowed when specifying a geometric region (e.g. POINT, CIRCLE,
+ * 		REGION) ; <i><b>note:</b> this function is mainly useful with ADQL-2.0
+ * 		because it is the only version in which the coordinate system parameter
+ * 		is mandatory</i></li>
  * </ul>
  *
+ * <h3>Default general checks</h3>
+ *
+ * <p>
+ * 	This ADQL parser always performs some verification after the parsing of an
+ * 	ADQL query. In addition of the syntax, this parser also ensures that no
+ * 	unsupported optional language feature and no unsupported coordinate system
+ * 	are used in parsed ADQL queries. If unsupported content is detected, a
+ * 	{@link ParseException} is immediately raised.
+ * </p>
+ *
+ * <p><i><b>Note:</b>
+ * 	By default, all optional language features are supported, and any UDF and
+ * 	coordinate system are allowed.
+ * </i></p>
+ *
+ *
  * <h3>Custom checks</h3>
  *
  * <p>
- *   This parser is able, thanks to a {@link QueryChecker} object, to check each
- *   {@link ADQLQuery} just after its generation. It could be used, for
- *   instance, to check the consistency between the ADQL query to parse and the
- *   "database" on which the query must be executed.
+ * 	Besides the general checks, this parser allows the addition of a custom
+ * 	validation. Thanks to a {@link QueryChecker} object, it is able to check
+ * 	each {@link ADQLQuery} just after its generation and the general checks.
+ * 	It could be used, for instance, to check the consistency between the ADQL
+ * 	query to parse and the "database" on which the query must be executed.
  * </p>
  *
  * <p>
@@ -162,9 +203,9 @@ import adql.translator.TranslationException;
  * <h3>Custom Query Factory</h3>
  *
  * <p>
- *   To create an object representation of the given ADQL query, this parser
- *   uses a {@link ADQLQueryFactory} object. All parts of the ADQL grammar can
- *   already be created with this object.
+ * 	To create an object representation of the given ADQL query, this parser
+ * 	uses a {@link ADQLQueryFactory} object. All parts of the ADQL grammar can
+ * 	already be created with this object.
  * </p>
  *
  * <p>
@@ -196,6 +237,35 @@ public class ADQLParser {
 	 * <p><i><b>Implementation note:</b> Never NULL.</i></p> */
 	protected FeatureSet supportedFeatures;
 
+	/** List of all allowed coordinate systems.
+	 * <p>
+	 * 	Each item of this list must be of the form: "{frame} {refpos} {flavor}".
+	 * 	Each of these 3 items can be either of value, a list of values expressed
+	 * 	with the syntax "({value1}|{value2}|...)" or a '*' to mean all possible
+	 * 	values.
+	 * </p>
+	 * <p><i><b>Note:</b>
+	 * 	since a default value (corresponding to the empty string - '') should
+	 * 	always be possible for each part of a coordinate system, the checker
+	 * 	will always add the default value (UNKNOWNFRAME, UNKNOWNREFPOS or
+	 * 	SPHERICAL2) into the given list of possible values for each coord. sys.
+	 * 	part.
+	 * </i></p>
+	 * <p>
+	 * 	If this list is NULL, all coordinates systems are allowed.
+	 * 	However, if not, all items of this list must be the only allowed
+	 * 	coordinate systems. So, if the list is empty, none is allowed.
+	 * </p> */
+	protected String[] allowedCoordSys = null;
+
+	/** A regular expression built using the list of allowed coordinate systems.
+	 * <p>
+	 * 	With this regex, it is possible to known whether a coordinate system
+	 * 	expression is allowed or not.
+	 * </p>
+	 * <p>If NULL, all coordinate systems are allowed.</p> */
+	protected String coordSysRegExp = null;
+
 	/** API to check {@link ADQLQuery ADQL queries} (sub-queries or not) just
 	 * after their generation.
 	 * <p><i><b>Note:</b>
@@ -547,6 +617,60 @@ public class ADQLParser {
 		supportedFeatures = features;
 	}
 
+	/**
+	 * Get the list of allowed coordinate systems.
+	 *
+	 * <p>
+	 * 	If NULL, any coordinate system is allowed.
+	 * 	But if empty array, no coordinate system is allowed.
+	 * </p>
+	 *
+	 * @return	All allowed coordinate systems.
+	 */
+	public final String[] getAllowedCoordSys() {
+		return allowedCoordSys;
+	}
+
+	/**
+	 * Set the list of allowed coordinate systems.
+	 *
+	 * <p>
+	 * 	Each item of this list must be of the form: "{frame} {refpos} {flavor}".
+	 * 	Each of these 3 items can be either of value, a list of values expressed
+	 * 	with the syntax "({value1}|{value2}|...)" or a '*' to mean all possible
+	 * 	values.
+	 * </p>
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	since a default value (corresponding to the empty string - '') should
+	 * 	always be possible for each part of a coordinate system, this parser
+	 * 	will always add the default value (UNKNOWNFRAME, UNKNOWNREFPOS or
+	 * 	SPHERICAL2) into the given list of possible values for each coord. sys.
+	 * 	part.
+	 * </i></p>
+	 *
+	 * <p>
+	 * 	If this list is NULL, all coordinates systems are allowed.
+	 * 	However, if not, all items of this list must be the only allowed
+	 * 	coordinate systems. So, if the list is empty, none is allowed.
+	 * </p>
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	When an exception is thrown, the list of allowed coordinate systems of
+	 * 	this parser stays in the same state than before calling this function.
+	 * </i></p>
+	 *
+	 * @param allowedCoordSys	List of allowed coordinate systems.
+	 *
+	 * @throws ParseException	If the syntax of one of the given coordinate
+	 *                       	system is incorrect.
+	 */
+	public final void setAllowedCoordSys(final Collection<String> allowedCoordSys) throws ParseException {
+		String[] tempAllowedCoordSys = specialSort(allowedCoordSys);
+		coordSysRegExp = STCS.buildCoordSysRegExp(tempAllowedCoordSys);
+		this.allowedCoordSys = tempAllowedCoordSys;
+	}
+
 	/**
 	 * Get the custom checker of parsed ADQL queries.
 	 *
@@ -681,50 +805,6 @@ public class ADQLParser {
 		return parsedQuery;
 	}
 
-	/**
-	 * Run the general and common checks on the given ADQL tree.
-	 *
-	 * <p>
-	 * 	By default, this function only checks whether or not language features
-	 * 	found in the given ADQL tree are supported. If not, a
-	 * 	{@link ParseException} is thrown.
-	 * </p>
-	 *
-	 * <p><i><b>Implementation note:</b>
-	 * 	By default, when a part of the ADQL tree uses an unsupported language
-	 * 	feature, an {@link UnsupportedFeatureException} is created. This
-	 * 	function does not stop at the first encountered unsupported feature. It
-	 * 	keeps reading the ADQL tree and appends all created
-	 * 	{@link UnsupportedFeatureException}s into an
-	 * 	{@link UnresolvedIdentifiersException}. Finally, when the tree has been
-	 * 	entirely read and if unsupported features have been encountered, this
-	 * 	{@link UnresolvedIdentifiersException} is thrown.
-	 * </p>
-	 *
-	 * @param q	The ADQL query to check.
-	 *
-	 * @throws ParseException	If any unsupported language feature is used in
-	 *                       	the given ADQL tree.
-	 */
-	protected void generalChecks(final ADQLQuery q) throws ParseException {
-		// Create the exception in which errors have to be appended:
-		UnresolvedIdentifiersException exUnsupportedFeatures = new UnresolvedIdentifiersException("unsupported expression");
-
-		// Search recursively for all optional features inside the given tree:
-		SearchOptionalFeaturesHandler sFeaturesHandler = new SearchOptionalFeaturesHandler(true, false);
-		sFeaturesHandler.search(q);
-
-		// Append an error for each unsupported one:
-		for(ADQLObject obj : sFeaturesHandler) {
-			if (!supportedFeatures.isSupporting(obj.getFeatureDescription()))
-				exUnsupportedFeatures.addException(new UnsupportedFeatureException(obj));
-		}
-
-		// If unsupported features have been found, throw a ParseException:
-		if (exUnsupportedFeatures.getNbErrors() > 0)
-			throw exUnsupportedFeatures;
-	}
-
 	/**
 	 * Parse the given <code>SELECT</code> clause.
 	 *
@@ -961,6 +1041,302 @@ public class ADQLParser {
 		}
 	}
 
+	/* **********************************************************************
+	   *                          QUERY CHECKS                              *
+	   ********************************************************************** */
+
+	/**
+	 * Run the general and common checks on the given ADQL tree.
+	 *
+	 * <p>
+	 * 	By default, this function checks whether or not language features
+	 * 	found in the given ADQL tree are supported. It also checks all explicit
+	 * 	coordinate systems and STC-s expressions (embedded in REGION function).
+	 * 	All unsupported expressions (i.e. feature, coord. sys., STC-s) are
+	 * 	appended into an {@link UnresolvedIdentifiersException} which is finally
+	 * 	thrown if not empty.
+	 * </p>
+	 *
+	 * @param q	The ADQL query to check.
+	 *
+	 * @throws ParseException	If any unsupported language feature is used in
+	 *                       	the given ADQL tree.
+	 */
+	protected void generalChecks(final ADQLQuery q) throws ParseException {
+		// Create the exception in which errors have to be appended:
+		UnresolvedIdentifiersException exUnsupportedFeatures = new UnresolvedIdentifiersException("unsupported expression");
+
+		// Search recursively for all optional features inside the given tree:
+		SearchOptionalFeaturesHandler sFeaturesHandler = new SearchOptionalFeaturesHandler(true, false);
+		sFeaturesHandler.search(q);
+
+		// Append an error for each unsupported one:
+		for(ADQLObject obj : sFeaturesHandler) {
+			if (!supportedFeatures.isSupporting(obj.getFeatureDescription()))
+				exUnsupportedFeatures.addException(new UnsupportedFeatureException(obj));
+		}
+
+		// [only for ADQL-2.0] Resolve explicit coordinate system declarations:
+		resolveCoordinateSystems(q, exUnsupportedFeatures);
+
+		// [only for ADQL-2.0] Resolve explicit REGION declarations:
+		if (supportedFeatures.isSupporting(RegionFunction.FEATURE))
+			resolveSTCSExpressions(q, exUnsupportedFeatures);
+
+		// If unsupported features have been found, throw a ParseException:
+		if (exUnsupportedFeatures.getNbErrors() > 0)
+			throw exUnsupportedFeatures;
+	}
+
+	/**
+	 * Transform the given collection of string elements into a sorted array.
+	 * Only non-NULL and non-empty strings are kept.
+	 *
+	 * @param items	Items to copy and sort.
+	 *
+	 * @return	A sorted array containing all - except NULL and empty strings -
+	 *        	items of the given collection.
+	 */
+	protected final String[] specialSort(final Collection<String> items) {
+		// Nothing to do if the array is NULL:
+		if (items == null)
+			return null;
+
+		// Keep only valid items (not NULL and not empty string):
+		String[] tmp = new String[items.size()];
+		int cnt = 0;
+		for(String item : items) {
+			if (item != null && item.trim().length() > 0)
+				tmp[cnt++] = item;
+		}
+
+		// Make an adjusted array copy:
+		String[] copy = new String[cnt];
+		System.arraycopy(tmp, 0, copy, 0, cnt);
+
+		// Sort the values:
+		Arrays.sort(copy);
+
+		return copy;
+	}
+
+	/**
+	 * Search for all explicit coordinate system declarations, check their
+	 * syntax and whether they are allowed by this implementation.
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	"explicit" means here that all {@link StringConstant} instances. Only
+	 * 	coordinate systems expressed as string can be parsed and so checked. So
+	 * 	if a coordinate system is specified by a column, no check can be done at
+	 * 	this stage...it will be possible to perform such test only at the
+	 * 	execution.
+	 * </i></p>
+	 *
+	 * @param query		Query in which coordinate systems must be checked.
+	 * @param errors	List of errors to complete in this function each time a
+	 *              	coordinate system has a wrong syntax or is not
+	 *              	supported.
+	 *
+	 * @see #checkCoordinateSystem(StringConstant, UnresolvedIdentifiersException)
+	 */
+	protected void resolveCoordinateSystems(final ADQLQuery query, final UnresolvedIdentifiersException errors) {
+		ISearchHandler sHandler = new SearchCoordSysHandler();
+		sHandler.search(query);
+		for(ADQLObject result : sHandler)
+			checkCoordinateSystem((StringConstant)result, errors);
+	}
+
+	/**
+	 * Parse and then check the coordinate system contained in the given
+	 * {@link StringConstant} instance.
+	 *
+	 * @param adqlCoordSys	The {@link StringConstant} object containing the
+	 *                    	coordinate system to check.
+	 * @param errors		List of errors to complete in this function each
+	 *              		time a coordinate system has a wrong syntax or is
+	 *              		not supported.
+	 *
+	 * @see STCS#parseCoordSys(String)
+	 * @see #checkCoordinateSystem(CoordSys, ADQLOperand, UnresolvedIdentifiersException)
+	 */
+	protected void checkCoordinateSystem(final StringConstant adqlCoordSys, final UnresolvedIdentifiersException errors) {
+		String coordSysStr = adqlCoordSys.getValue();
+		try {
+			checkCoordinateSystem(STCS.parseCoordSys(coordSysStr), adqlCoordSys, errors);
+		} catch(ParseException pe) {
+			errors.addException(new ParseException(pe.getMessage(), adqlCoordSys.getPosition()));
+		}
+	}
+
+	/**
+	 * Check whether the given coordinate system is allowed by this
+	 * implementation.
+	 *
+	 * @param coordSys	Coordinate system to test.
+	 * @param operand	The operand representing or containing the coordinate
+	 *               	system under test.
+	 * @param errors	List of errors to complete in this function each time a
+	 *              	coordinate system is not supported.
+	 */
+	protected void checkCoordinateSystem(final CoordSys coordSys, final ADQLOperand operand, final UnresolvedIdentifiersException errors) {
+		if (coordSysRegExp != null && coordSys != null && !coordSys.toFullSTCS().matches(coordSysRegExp)) {
+			StringBuffer buf = new StringBuffer();
+			if (allowedCoordSys != null) {
+				for(String cs : allowedCoordSys) {
+					if (buf.length() > 0)
+						buf.append(", ");
+					buf.append(cs);
+				}
+			}
+			if (buf.length() == 0)
+				buf.append("No coordinate system is allowed!");
+			else
+				buf.insert(0, "Allowed coordinate systems are: ");
+			errors.addException(new ParseException("Coordinate system \"" + ((operand instanceof StringConstant) ? ((StringConstant)operand).getValue() : coordSys.toString()) + "\" (= \"" + coordSys.toFullSTCS() + "\") not allowed in this implementation. " + buf.toString(), operand.getPosition()));
+		}
+	}
+
+	/**
+	 * Search for all STC-S expressions inside the given query, parse them (and
+	 * so check their syntax) and then determine whether the declared coordinate
+	 * system and the expressed region are allowed in this implementation.
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	In the current ADQL language definition, STC-S expressions can be found
+	 * 	only as only parameter of the REGION function.
+	 * </i></p>
+	 *
+	 * @param query		Query in which STC-S expressions must be checked.
+	 * @param errors	List of errors to complete in this function each time
+	 *              	the STC-S syntax is wrong or each time the declared
+	 *              	coordinate system or region is not supported.
+	 *
+	 * @see STCS#parseRegion(String)
+	 * @see #checkRegion(Region, RegionFunction, UnresolvedIdentifiersException)
+	 */
+	protected void resolveSTCSExpressions(final ADQLQuery query, final UnresolvedIdentifiersException errors) {
+		// Search REGION functions:
+		ISearchHandler sHandler = new SearchRegionHandler();
+		sHandler.search(query);
+
+		// Parse and check their STC-S expression:
+		String stcs;
+		Region region;
+		for(ADQLObject result : sHandler) {
+			try {
+				// get the STC-S expression:
+				stcs = ((StringConstant)((RegionFunction)result).getParameter(0)).getValue();
+
+				// parse the STC-S expression (and so check the syntax):
+				region = STCS.parseRegion(stcs);
+
+				// check whether the regions (this one + the possible inner ones) and the coordinate systems are allowed:
+				checkRegion(region, (RegionFunction)result, errors);
+			} catch(ParseException pe) {
+				errors.addException(new ParseException(pe.getMessage(), result.getPosition()));
+			}
+		}
+	}
+
+	/**
+	 * Check the given region.
+	 *
+	 * <p>The following points are checked in this function:</p>
+	 * <ul>
+	 * 	<li>whether the coordinate system is allowed,</li>
+	 * 	<li>whether the type of region is allowed,</li>
+	 * 	<li>and whether the inner regions are correct (here this function is
+	 * 		called recursively on each inner region).</li>
+	 * </ul>
+	 *
+	 * @param r			The region to check.
+	 * @param fct		The REGION function containing the region to check.
+	 * @param errors	List of errors to complete in this function if the given
+	 *              	region or its inner regions are not supported.
+	 *
+	 * @see #checkCoordinateSystem(CoordSys, ADQLOperand, UnresolvedIdentifiersException)
+	 * @see #checkRegion(Region, RegionFunction, UnresolvedIdentifiersException)
+	 */
+	protected void checkRegion(final Region r, final RegionFunction fct, final UnresolvedIdentifiersException errors) {
+		if (r == null)
+			return;
+
+		// Check the coordinate system (if any):
+		if (r.coordSys != null)
+			checkCoordinateSystem(r.coordSys, fct, errors);
+
+		// Check that the region type is allowed:
+		LanguageFeature feature;
+		switch(r.type) {
+			case POSITION:
+				feature = PointFunction.FEATURE;
+				break;
+			case BOX:
+				feature = BoxFunction.FEATURE;
+				break;
+			case CIRCLE:
+				feature = CircleFunction.FEATURE;
+				break;
+			case POLYGON:
+				feature = PolygonFunction.FEATURE;
+				break;
+			default:
+				/* TODO Add a case for UNION and INTERSECT when supported! */
+				feature = null;
+				break;
+		}
+		if (r.type != RegionType.NOT && (feature == null || !supportedFeatures.isSupporting(feature)))
+			errors.addException(new UnsupportedFeatureException(fct, "Unsupported STC-s region type: \"" + r.type + "\"" + (feature == null ? "!" : " (equivalent to the ADQL feature \"" + feature.form + "\" of type '" + feature.type + "')!")));
+
+		// Check all the inner regions:
+		if (r.regions != null) {
+			for(Region innerR : r.regions)
+				checkRegion(innerR, fct, errors);
+		}
+	}
+
+	/**
+	 * Let searching all explicit declarations of coordinate system.
+	 * So, only {@link StringConstant} objects will be returned.
+	 *
+	 * @author Gr&eacute;gory Mantelet (CDS)
+	 * @version 2.0 (08/2019)
+	 * @since 2.0
+	 */
+	private static class SearchCoordSysHandler extends SimpleSearchHandler {
+		@Override
+		protected boolean match(ADQLObject obj) {
+			if (obj instanceof PointFunction || obj instanceof BoxFunction || obj instanceof CircleFunction || obj instanceof PolygonFunction)
+				return (((GeometryFunction)obj).getCoordinateSystem() instanceof StringConstant);
+			else
+				return false;
+		}
+
+		@Override
+		protected void addMatch(ADQLObject matchObj, ADQLIterator it) {
+			results.add(((GeometryFunction)matchObj).getCoordinateSystem());
+		}
+
+	}
+
+	/**
+	 * Let searching all {@link RegionFunction}s.
+	 *
+	 * @author Gr&eacute;gory Mantelet (CDS)
+	 * @version 2.0 (08/2019)
+	 * @since 2.0
+	 */
+	private static class SearchRegionHandler extends SimpleSearchHandler {
+		@Override
+		protected boolean match(ADQLObject obj) {
+			if (obj instanceof RegionFunction)
+				return (((RegionFunction)obj).getParameter(0) instanceof StringConstant);
+			else
+				return false;
+		}
+
+	}
+
 	/* **********************************************************************
 	   *                     TOKENIZATION FUNCTION                          *
 	   ********************************************************************** */
diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java
index f0ab7ec..04806f4 100644
--- a/test/adql/db/TestDBChecker.java
+++ b/test/adql/db/TestDBChecker.java
@@ -31,10 +31,6 @@ import adql.query.operand.ADQLOperand;
 import adql.query.operand.StringConstant;
 import adql.query.operand.function.DefaultUDF;
 import adql.query.operand.function.UserDefinedFunction;
-import adql.query.operand.function.geometry.CircleFunction;
-import adql.query.operand.function.geometry.ContainsFunction;
-import adql.query.operand.function.geometry.PointFunction;
-import adql.query.operand.function.geometry.RegionFunction;
 import adql.search.SimpleSearchHandler;
 import adql.translator.ADQLTranslator;
 import adql.translator.PostgreSQLTranslator;
@@ -405,181 +401,6 @@ public class TestDBChecker {
 		}
 	}
 
-	@Test
-	public void testGeometry() {
-		// DECLARE A SIMPLE PARSER where all geometries are allowed by default:
-		ADQLParser parser = new ADQLParser();
-		parser.setQueryChecker(new DBChecker(tables));
-
-		// Test with several geometries while all are allowed:
-		try {
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
-		}
-
-		// Test with several geometries while only the allowed ones:
-		try {
-			parser = new ADQLParser();
-			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
-			parser.getSupportedFeatures().support(ContainsFunction.FEATURE);
-			parser.getSupportedFeatures().support(PointFunction.FEATURE);
-			parser.getSupportedFeatures().support(CircleFunction.FEATURE);
-
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
-		}
-		try {
-			parser.parseQuery("SELECT * FROM foo WHERE INTERSECTS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;");
-			fail("This query contains a not-allowed geometry function (INTERSECTS): this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(1, ex.getNbErrors());
-			assertEquals("Unsupported ADQL feature: \"INTERSECTS\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", ex.getErrors().next().getMessage());
-		}
-
-		// Test by adding REGION:
-		try {
-			parser = new ADQLParser();
-			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
-			parser.getSupportedFeatures().support(ContainsFunction.FEATURE);
-			parser.getSupportedFeatures().support(PointFunction.FEATURE);
-			parser.getSupportedFeatures().support(CircleFunction.FEATURE);
-			parser.getSupportedFeatures().support(RegionFunction.FEATURE);
-
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
-		}
-		// TODO Deal with un/supported Regions inside STC-S!
-		try {
-			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('BOX 1.2 2.3 5 9')) = 1;");
-			fail("This query contains a not-allowed geometry function (BOX): this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(1, ex.getNbErrors());
-			assertEquals("The geometrical function \"BOX\" is not available in this implementation!", ex.getErrors().next().getMessage());
-		}
-
-		// Test with several geometries while none geometry is allowed:
-		try {
-			parser = new ADQLParser();
-			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
-
-			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;");
-			fail("This query contains geometries while they are all forbidden: this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(3, ex.getNbErrors());
-			Iterator<ParseException> itErrors = ex.getErrors();
-			assertEquals("Unsupported ADQL feature: \"CONTAINS\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
-			assertEquals("Unsupported ADQL feature: \"POINT\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
-			assertEquals("Unsupported ADQL feature: \"CIRCLE\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
-		}
-	}
-
-	@Test
-	public void testCoordSys() {
-		// DECLARE A SIMPLE PARSER where all coordinate systems are allowed by default:
-		ADQLParser parser = new ADQLParser();
-		parser.setQueryChecker(new DBChecker(tables));
-
-		// Test with several coordinate systems while all are allowed:
-		try {
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('lsr', 12.3, 45.6), CIRCLE('galactic heliocenter', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('unknownframe', 12.3, 45.6), CIRCLE('galactic unknownrefpos spherical2', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position icrs lsr 12.3 45.6'), REGION('circle fk5 1.2 2.3 5')) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!");
-		}
-
-		// Concatenation as coordinate systems not checked:
-		try {
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('From ' || 'here', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains a concatenation as coordinate systems (but only string constants are checked): this test should have succeeded!");
-		}
-
-		// Test with several coordinate systems while only some allowed:
-		try {
-			parser = new ADQLParser();
-			parser.setQueryChecker(new DBChecker(tables, new ArrayList<FunctionDef>(0), null, Arrays.asList(new String[]{ "icrs * *", "fk4 geocenter *", "galactic * spherical2" })));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian3', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT POINT('fk4', 12.3, 45.6) FROM foo;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('fk4 geocenter', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic', 12.3, 45.6), CIRCLE('galactic spherical2', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic geocenter', 12.3, 45.6), CIRCLE('galactic lsr spherical2', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position galactic lsr 12.3 45.6'), REGION('circle icrs 1.2 2.3 5')) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!");
-		}
-		try {
-			parser.parseQuery("SELECT POINT('fk5 geocenter', 12.3, 45.6) FROM foo;");
-			fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(1, ex.getNbErrors());
-			assertEquals("Coordinate system \"fk5 geocenter\" (= \"FK5 GEOCENTER SPHERICAL2\") not allowed in this implementation. Allowed coordinate systems are: fk4 geocenter *, galactic * spherical2, icrs * *", ex.getErrors().next().getMessage());
-		}
-		try {
-			parser.parseQuery("SELECT Region('not(position fk5 heliocenter 1 2)') FROM foo;");
-			fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(1, ex.getNbErrors());
-			assertEquals("Coordinate system \"FK5 HELIOCENTER\" (= \"FK5 HELIOCENTER SPHERICAL2\") not allowed in this implementation. Allowed coordinate systems are: fk4 geocenter *, galactic * spherical2, icrs * *", ex.getErrors().next().getMessage());
-		}
-
-		// Test with a coordinate system while none is allowed:
-		try {
-			parser = new ADQLParser();
-			parser.setQueryChecker(new DBChecker(tables, new ArrayList<FunctionDef>(0), null, new ArrayList<String>(0)));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;"));
-			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
-		} catch(ParseException pe) {
-			pe.printStackTrace();
-			fail("This query specifies none coordinate system: this test should have succeeded!");
-		}
-		try {
-			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('ICRS SPHERICAL2', 12.3, 45.6), CIRCLE('icrs', 1.2, 2.3, 5)) = 1;");
-			fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(2, ex.getNbErrors());
-			Iterator<ParseException> itErrors = ex.getErrors();
-			assertEquals("Coordinate system \"ICRS SPHERICAL2\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", itErrors.next().getMessage());
-			assertEquals("Coordinate system \"icrs\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", itErrors.next().getMessage());
-		}
-		try {
-			parser.parseQuery("SELECT Region('not(position fk4 1 2)') FROM foo;");
-			fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!");
-		} catch(ParseException pe) {
-			assertTrue(pe instanceof UnresolvedIdentifiersException);
-			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
-			assertEquals(1, ex.getNbErrors());
-			assertEquals("Coordinate system \"FK4\" (= \"FK4 UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", ex.getErrors().next().getMessage());
-		}
-	}
-
 	@Test
 	public void testTypesChecking() {
 		// DECLARE A SIMPLE PARSER:
diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java
index 423b9c3..6c4102b 100644
--- a/test/adql/parser/TestADQLParser.java
+++ b/test/adql/parser/TestADQLParser.java
@@ -5,12 +5,17 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import adql.db.FunctionDef;
 import adql.db.exception.UnresolvedIdentifiersException;
 import adql.db.exception.UnsupportedFeatureException;
 import adql.parser.ADQLParser.ADQLVersion;
@@ -22,7 +27,10 @@ import adql.query.ADQLQuery;
 import adql.query.from.ADQLJoin;
 import adql.query.from.ADQLTable;
 import adql.query.operand.StringConstant;
+import adql.query.operand.function.geometry.CircleFunction;
+import adql.query.operand.function.geometry.ContainsFunction;
 import adql.query.operand.function.geometry.PointFunction;
+import adql.query.operand.function.geometry.RegionFunction;
 import adql.query.operand.function.string.LowerFunction;
 
 public class TestADQLParser {
@@ -331,7 +339,6 @@ public class TestADQLParser {
 	@Test
 	public void testUDFName() {
 		ADQLParser parser = new ADQLParser();
-		// TODO [ADQL-2.1] Add the support for this specific UDF in the the FeatureSet!
 
 		// CASE: Valid UDF name => OK
 		try {
@@ -354,6 +361,39 @@ public class TestADQLParser {
 		}
 	}
 
+	@Test
+	public void testUDFDeclaration() {
+		ADQLParser parser = new ADQLParser();
+
+		// CASE: Any UDF allowed => OK!
+		parser.getSupportedFeatures().allowAnyUdf(true);
+		try {
+			assertNotNull(parser.parseQuery("SELECT foo(1,2) FROM bar"));
+		} catch(Throwable t) {
+			t.printStackTrace();
+			fail("Unexpected parsing error! This query should have passed. (see console for more details)");
+		}
+
+		// CASE: No UDF allowed => ERROR
+		parser.getSupportedFeatures().allowAnyUdf(false);
+		try {
+			parser.parseQuery("SELECT foo(1,2) FROM bar");
+			fail("No UDF is allowed. This query should have failed!");
+		} catch(Throwable t) {
+			assertEquals(UnresolvedIdentifiersException.class, t.getClass());
+			assertEquals("1 unsupported expressions!\n  - Unsupported ADQL feature: \"foo(param1 ?type?, param2 ?type?) -> ?type?\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-udf')!", t.getMessage());
+		}
+
+		// CASE: a single UDF declared => OK!
+		try {
+			parser.getSupportedFeatures().support(FunctionDef.parse("foo(i1 INTEGER, i2 INTEGER) -> INTEGER").toLanguageFeature());
+			assertNotNull(parser.parseQuery("SELECT foo(1,2) FROM bar"));
+		} catch(Throwable t) {
+			t.printStackTrace();
+			fail("Unexpected parsing error! This query should have passed. (see console for more details)");
+		}
+	}
+
 	@Test
 	public void testOptionalFeatures() {
 		ADQLParser parser = new ADQLParser(ADQLVersion.V2_0);
@@ -432,6 +472,178 @@ public class TestADQLParser {
 		}
 	}
 
+	@Test
+	public void testGeometry() {
+		// DECLARE A SIMPLE PARSER where all geometries are allowed by default:
+		ADQLParser parser = new ADQLParser();
+
+		// Test with several geometries while all are allowed:
+		try {
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
+		}
+
+		// Test with several geometries while only the allowed ones:
+		try {
+			parser = new ADQLParser();
+			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
+			parser.getSupportedFeatures().support(ContainsFunction.FEATURE);
+			parser.getSupportedFeatures().support(PointFunction.FEATURE);
+			parser.getSupportedFeatures().support(CircleFunction.FEATURE);
+
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
+		}
+		try {
+			parser.parseQuery("SELECT * FROM foo WHERE INTERSECTS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;");
+			fail("This query contains a not-allowed geometry function (INTERSECTS): this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(1, ex.getNbErrors());
+			assertEquals("Unsupported ADQL feature: \"INTERSECTS\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", ex.getErrors().next().getMessage());
+		}
+
+		// Test by adding REGION:
+		try {
+			parser = new ADQLParser();
+			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
+			parser.getSupportedFeatures().support(ContainsFunction.FEATURE);
+			parser.getSupportedFeatures().support(PointFunction.FEATURE);
+			parser.getSupportedFeatures().support(CircleFunction.FEATURE);
+			parser.getSupportedFeatures().support(RegionFunction.FEATURE);
+
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!");
+		}
+		try {
+			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('BOX 1.2 2.3 5 9')) = 1;");
+			fail("This query contains a not-allowed geometry function (BOX): this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(1, ex.getNbErrors());
+			assertEquals("Unsupported STC-s region type: \"BOX\" (equivalent to the ADQL feature \"BOX\" of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", ex.getErrors().next().getMessage());
+		}
+
+		// Test with several geometries while none geometry is allowed:
+		try {
+			parser = new ADQLParser();
+			parser.getSupportedFeatures().unsupportAll(LanguageFeature.TYPE_ADQL_GEO);
+
+			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;");
+			fail("This query contains geometries while they are all forbidden: this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(3, ex.getNbErrors());
+			Iterator<ParseException> itErrors = ex.getErrors();
+			assertEquals("Unsupported ADQL feature: \"CONTAINS\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
+			assertEquals("Unsupported ADQL feature: \"POINT\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
+			assertEquals("Unsupported ADQL feature: \"CIRCLE\" (of type 'ivo://ivoa.net/std/TAPRegExt#features-adql-geo')!", itErrors.next().getMessage());
+		}
+	}
+
+	@Test
+	public void testCoordSys() {
+		// DECLARE A SIMPLE PARSER where all coordinate systems are allowed by default:
+		ADQLParser parser = new ADQLParser();
+
+		// Test with several coordinate systems while all are allowed:
+		try {
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('lsr', 12.3, 45.6), CIRCLE('galactic heliocenter', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('unknownframe', 12.3, 45.6), CIRCLE('galactic unknownrefpos spherical2', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position icrs lsr 12.3 45.6'), REGION('circle fk5 1.2 2.3 5')) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!");
+		}
+
+		// Concatenation as coordinate systems not checked:
+		try {
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('From ' || 'here', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains a concatenation as coordinate systems (but only string constants are checked): this test should have succeeded!");
+		}
+
+		// Test with several coordinate systems while only some allowed:
+		try {
+			parser = new ADQLParser();
+			parser.setAllowedCoordSys(Arrays.asList(new String[]{ "icrs * *", "fk4 geocenter *", "galactic * spherical2" }));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian3', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT POINT('fk4', 12.3, 45.6) FROM foo;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('fk4 geocenter', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic', 12.3, 45.6), CIRCLE('galactic spherical2', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic geocenter', 12.3, 45.6), CIRCLE('galactic lsr spherical2', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position galactic lsr 12.3 45.6'), REGION('circle icrs 1.2 2.3 5')) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!");
+		}
+		try {
+			parser.parseQuery("SELECT POINT('fk5 geocenter', 12.3, 45.6) FROM foo;");
+			fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(1, ex.getNbErrors());
+			assertEquals("Coordinate system \"fk5 geocenter\" (= \"FK5 GEOCENTER SPHERICAL2\") not allowed in this implementation. Allowed coordinate systems are: fk4 geocenter *, galactic * spherical2, icrs * *", ex.getErrors().next().getMessage());
+		}
+		try {
+			parser.parseQuery("SELECT Region('not(position fk5 heliocenter 1 2)') FROM foo;");
+			fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(1, ex.getNbErrors());
+			assertEquals("Coordinate system \"FK5 HELIOCENTER\" (= \"FK5 HELIOCENTER SPHERICAL2\") not allowed in this implementation. Allowed coordinate systems are: fk4 geocenter *, galactic * spherical2, icrs * *", ex.getErrors().next().getMessage());
+		}
+
+		// Test with a coordinate system while none is allowed:
+		try {
+			parser = new ADQLParser();
+			parser.setAllowedCoordSys(new ArrayList<String>(0));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;"));
+			assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;"));
+		} catch(ParseException pe) {
+			pe.printStackTrace();
+			fail("This query specifies none coordinate system: this test should have succeeded!");
+		}
+		try {
+			parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('ICRS SPHERICAL2', 12.3, 45.6), CIRCLE('icrs', 1.2, 2.3, 5)) = 1;");
+			fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(2, ex.getNbErrors());
+			Iterator<ParseException> itErrors = ex.getErrors();
+			assertEquals("Coordinate system \"ICRS SPHERICAL2\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", itErrors.next().getMessage());
+			assertEquals("Coordinate system \"icrs\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", itErrors.next().getMessage());
+		}
+		try {
+			parser.parseQuery("SELECT Region('not(position fk4 1 2)') FROM foo;");
+			fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!");
+		} catch(ParseException pe) {
+			assertTrue(pe instanceof UnresolvedIdentifiersException);
+			UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe;
+			assertEquals(1, ex.getNbErrors());
+			assertEquals("Coordinate system \"FK4\" (= \"FK4 UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation. No coordinate system is allowed!", ex.getErrors().next().getMessage());
+		}
+	}
+
 	@Test
 	public void testTokenize() {
 		ADQLParser parser = new ADQLParser(ADQLVersion.V2_0);
-- 
GitLab