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é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é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é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é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é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