package adql.db; /* * This file is part of ADQLLibrary. * * ADQLLibrary is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ADQLLibrary is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2011-2017 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Stack; import adql.db.STCS.CoordSys; import adql.db.STCS.Region; import adql.db.STCS.RegionType; import adql.db.exception.UnresolvedColumnException; import adql.db.exception.UnresolvedFunctionException; import adql.db.exception.UnresolvedIdentifiersException; import adql.db.exception.UnresolvedTableException; import adql.parser.ParseException; import adql.parser.QueryChecker; import adql.query.ADQLIterator; import adql.query.ADQLObject; import adql.query.ADQLQuery; import adql.query.ClauseADQL; import adql.query.ClauseSelect; import adql.query.ColumnReference; import adql.query.IdentifierField; import adql.query.SelectAllColumns; import adql.query.SelectItem; import adql.query.from.ADQLTable; import adql.query.from.FromContent; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; import adql.query.operand.StringConstant; import adql.query.operand.UnknownType; import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.DefaultUDF; import adql.query.operand.function.UserDefinedFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CircleFunction; 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.SearchColumnHandler; import adql.search.SimpleReplaceHandler; import adql.search.SimpleSearchHandler; /** * This {@link QueryChecker} implementation is able to do the following verifications on an ADQL query: * <ol> * <li>Check the existence of all table and column references found in a query</li> * <li>Resolve all unknown functions as supported User Defined Functions (UDFs)</li> * <li>Check whether all used geometrical functions are supported</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> * * <h3>Check tables and columns</h3> * <p> * In addition to check the existence of tables and columns referenced in the query, * this checked will also attach database metadata on these references ({@link ADQLTable} * and {@link ADQLColumn} instances when they are resolved. * </p> * * <p>These information are:</p> * <ul> * <li>the corresponding {@link DBTable} or {@link DBColumn} (see getter and setter for DBLink in {@link ADQLTable} and {@link ADQLColumn})</li> * <li>the link between an {@link ADQLColumn} and its {@link ADQLTable}</li> * </ul> * * <p><i><u>Note:</u> * Knowing DB metadata of {@link ADQLTable} and {@link ADQLColumn} is 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}. * </i></p> * * @author Grégory Mantelet (CDS;ARI) * @version 1.4 (04/2017) */ public class DBChecker implements QueryChecker { /** List of all available tables ({@link DBTable}). */ protected SearchTableList lstTables; /** <p>List of all allowed geometrical functions (i.e. CONTAINS, REGION, POINT, COORD2, ...).</p> * <p> * If this list is NULL, all geometrical functions are allowed. * However, if not, all items of this list must be the only allowed geometrical functions. * So, if the list is empty, no such function is allowed. * </p> * @since 1.3 */ protected String[] allowedGeo = null; /** <p>List of all allowed coordinate systems.</p> * <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>Note: 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> * @since 1.3 */ 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 */ protected String coordSysRegExp = null; /** <p>List of all allowed User Defined Functions (UDFs).</p> * <p> * If this list is NULL, any encountered UDF will be allowed. * However, if not, all items of this list must be the only allowed UDFs. * So, if the list is empty, no UDF is allowed. * </p> * @since 1.3 */ protected FunctionDef[] allowedUdfs = null; /* ************ */ /* CONSTRUCTORS */ /* ************ */ /** * <p>Builds a {@link DBChecker} with an empty list of tables.</p> * * <p>Verifications done by this object after creation:</p> * <ul> * <li>Existence of tables and columns: <b>NO <i>(even unknown or fake tables and columns are allowed)</i></b></li> * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> * <li>Support of geometrical functions: <b>NO <i>(all valid geometrical functions are allowed)</i></b></li> * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> * </ul> */ public DBChecker(){ this(null, null); } /** * <p>Builds a {@link DBChecker} with the given list of known tables.</p> * * <p>Verifications done by this object after creation:</p> * <ul> * <li>Existence of tables and columns: <b>OK</b></li> * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> * <li>Support of geometrical functions: <b>NO <i>(all valid geometrical functions are allowed)</i></b></li> * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> * </ul> * * @param tables List of all available tables. */ public DBChecker(final Collection<? extends DBTable> tables){ this(tables, null); } /** * <p>Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.</p> * * <p>Verifications done by this object after creation:</p> * <ul> * <li>Existence of tables and columns: <b>OK</b></li> * <li>Existence of User Defined Functions (UDFs): <b>OK</b></li> * <li>Support of geometrical functions: <b>NO <i>(all valid geometrical functions are allowed)</i></b></li> * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> * </ul> * * @param tables List of all available tables. * @param allowedUdfs List of all allowed user defined functions. * If NULL, no verification will be done (and so, all UDFs are allowed). * If empty list, no "unknown" (or UDF) is allowed. * <i>Note: match with items of this list are done case insensitively.</i> * * @since 1.3 */ public DBChecker(final Collection<? extends DBTable> tables, final Collection<? extends FunctionDef> allowedUdfs){ // Sort and store the given tables: setTables(tables); Object[] tmp; int cnt; // Store all allowed UDFs in a sorted array: if (allowedUdfs != null){ // Remove all NULL and empty strings: tmp = new FunctionDef[allowedUdfs.size()]; cnt = 0; for(FunctionDef udf : allowedUdfs){ if (udf != null && udf.name.trim().length() > 0) tmp[cnt++] = udf; } // make a copy of the array: this.allowedUdfs = new FunctionDef[cnt]; System.arraycopy(tmp, 0, this.allowedUdfs, 0, cnt); tmp = null; // sort the values: Arrays.sort(this.allowedUdfs); } } /** * <p>Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.</p> * * <p>Verifications done by this object after creation:</p> * <ul> * <li>Existence of tables and columns: <b>OK</b></li> * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> * <li>Support of geometrical functions: <b>OK</b></li> * <li>Support of coordinate systems: <b>OK</b></li> * </ul> * * @param tables List of all available tables. * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). * If NULL, no verification will be done (and so, all geometries are allowed). * If empty list, no geometry function is allowed. * <i>Note: match with items of this list are done case insensitively.</i> * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. * Each part of this pattern can be one the possible values (case insensitive), a list of possible values * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. * For instance: "ICRS (GEOCENTER|heliocenter) *". * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). * * @since 1.3 */ public DBChecker(final Collection<? extends DBTable> tables, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException{ this(tables, null, allowedGeoFcts, allowedCoordSys); } /** * <p>Builds a {@link DBChecker}.</p> * * <p>Verifications done by this object after creation:</p> * <ul> * <li>Existence of tables and columns: <b>OK</b></li> * <li>Existence of User Defined Functions (UDFs): <b>OK</b></li> * <li>Support of geometrical functions: <b>OK</b></li> * <li>Support of coordinate systems: <b>OK</b></li> * </ul> * * @param tables List of all available tables. * @param allowedUdfs List of all allowed user defined functions. * If NULL, no verification will be done (and so, all UDFs are allowed). * If empty list, no "unknown" (or UDF) is allowed. * <i>Note: match with items of this list are done case insensitively.</i> * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). * If NULL, no verification will be done (and so, all geometries are allowed). * If empty list, no geometry function is allowed. * <i>Note: match with items of this list are done case insensitively.</i> * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. * Each part of this pattern can be one the possible values (case insensitive), a list of possible values * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. * For instance: "ICRS (GEOCENTER|heliocenter) *". * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). * * @since 1.3 */ public DBChecker(final Collection<? extends DBTable> tables, final Collection<? extends FunctionDef> allowedUdfs, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException{ // Set the list of available tables + Set the list of all known UDFs: this(tables, allowedUdfs); // Set the list of allowed geometrical functions: allowedGeo = specialSort(allowedGeoFcts); // Set the list of allowed coordinate systems: this.allowedCoordSys = specialSort(allowedCoordSys); coordSysRegExp = STCS.buildCoordSysRegExp(this.allowedCoordSys); } /** * Transform the given collection of string elements in 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. * * @since 1.3 */ protected final static 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; } /* ****** */ /* SETTER */ /* ****** */ /** * <p>Sets the list of all available tables.</p> * * <p><i><u>Note:</u> * Only if the given collection is NOT an instance of {@link SearchTableList}, * the collection will be copied inside a new {@link SearchTableList}, otherwise it is used as provided. * </i></p> * * @param tables List of {@link DBTable}s. */ public final void setTables(final Collection<? extends DBTable> tables){ if (tables == null) lstTables = new SearchTableList(); else if (tables instanceof SearchTableList) lstTables = (SearchTableList)tables; else lstTables = new SearchTableList(tables); } /* ************* */ /* CHECK METHODS */ /* ************* */ /** * <p>Check all the columns, tables and UDFs references inside the given query.</p> * * <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. * </i></p> * * @param query The query to check. * * @throws ParseException An {@link UnresolvedIdentifiersException} if some tables or columns can not be resolved. * * @see #check(ADQLQuery, Stack) */ @Override public final void check(final ADQLQuery query) throws ParseException{ check(query, null); } /** * <p>Process several (semantic) verifications in the given ADQL query.</p> * * <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> * </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). * * @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{ UnresolvedIdentifiersException errors = new UnresolvedIdentifiersException(); // A. Check DB items (tables and columns): SearchColumnList availableColumns = checkDBItems(query, fathersList, errors); // B. Check UDFs: if (allowedUdfs != null) checkUDFs(query, errors); // C. Check geometries: checkGeometries(query, errors); // D. Check types: checkTypes(query, errors); // E. Check sub-queries: checkSubQueries(query, fathersList, availableColumns, errors); // Throw all errors, if any: if (errors.getNbErrors() > 0) throw errors; } /* ************************************************ */ /* CHECKING METHODS FOR DB ITEMS (TABLES & COLUMNS) */ /* ************************************************ */ /** * <p>Check DB items (tables and columns) used in the given ADQL query.</p> * * <p>Operations done in this function:</p> * <ol> * <li>Resolve all found tables</li> * <li>Get the whole list of all available columns <i>Note: this list is returned by this function.</i></li> * <li>Resolve all found columns</li> * </ol> * * @param query Query in which the existence of DB items must be checked. * @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> * @param errors List of errors to complete in this function each time an unknown table or column is encountered. * * @return List of all columns available in the given query. * * @see #resolveTables(ADQLQuery, Stack, UnresolvedIdentifiersException) * @see FromContent#getDBColumns() * @see #resolveColumns(ADQLQuery, Stack, Map, SearchColumnList, UnresolvedIdentifiersException) * * @since 1.3 */ protected SearchColumnList checkDBItems(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors){ // a. Resolve all tables: Map<DBTable,ADQLTable> mapTables = resolveTables(query, fathersList, errors); // b. Get the list of all columns made available in the clause FROM: SearchColumnList availableColumns; try{ availableColumns = query.getFrom().getDBColumns(); }catch(ParseException pe){ errors.addException(pe); availableColumns = new SearchColumnList(); } // c. Resolve all columns: resolveColumns(query, fathersList, mapTables, availableColumns, errors); return availableColumns; } /** * <p>Search all table references inside the given query, resolve them against the available tables, and if there is only one match, * attach the matching metadata to them.</p> * * <b>Management of sub-query tables</b> * <p> * If a table is not a DB table reference but a sub-query, this latter is first checked (using {@link #check(ADQLQuery, Stack)} ; * but the father list must not contain tables of the given query, because on the same level) and then corresponding table metadata * are generated (using {@link #generateDBTable(ADQLQuery, String)}) and attached to it. * </p> * * <b>Management of "{table}.*" in the SELECT clause</b> * <p> * For each of this SELECT item, this function tries to resolve the table name. If only one match is found, the corresponding ADQL table object * is got from the list of resolved tables and attached to this SELECT item (thus, the joker item will also have the good metadata, * particularly if the referenced table is a sub-query). * </p> * * @param query Query in which the existence of tables must be checked. * @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> * @param errors List of errors to complete in this function each time an unknown table or column is encountered. * * @return An associative map of all the resolved tables. */ protected Map<DBTable,ADQLTable> resolveTables(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors){ HashMap<DBTable,ADQLTable> mapTables = new HashMap<DBTable,ADQLTable>(); ISearchHandler sHandler; // Check the existence of all tables: sHandler = new SearchTableHandler(); sHandler.search(query.getFrom()); for(ADQLObject result : sHandler){ try{ ADQLTable table = (ADQLTable)result; // resolve the table: DBTable dbTable = null; if (table.isSubQuery()){ // check the sub-query tables: check(table.getSubQuery(), fathersList); // generate its DBTable: dbTable = generateDBTable(table.getSubQuery(), table.getAlias()); }else{ dbTable = resolveTable(table); if (table.hasAlias()) dbTable = dbTable.copy(null, table.getAlias()); } // link with the matched DBTable: table.setDBLink(dbTable); mapTables.put(dbTable, table); }catch(ParseException pe){ errors.addException(pe); } } // Attach table information on wildcards with the syntax "{tableName}.*" of the SELECT clause: /* Note: no need to check the table name among the father tables, because there is * no interest to select a father column in a sub-query * (which can return only one column ; besides, no aggregate is allowed * in sub-queries).*/ sHandler = new SearchWildCardHandler(); sHandler.search(query.getSelect()); for(ADQLObject result : sHandler){ try{ SelectAllColumns wildcard = (SelectAllColumns)result; ADQLTable table = wildcard.getAdqlTable(); DBTable dbTable = null; // first, try to resolve the table by table alias: if (table.getTableName() != null && table.getSchemaName() == null){ ArrayList<ADQLTable> tables = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE)); if (tables.size() == 1) dbTable = tables.get(0).getDBLink(); } // then try to resolve the table reference by table name: if (dbTable == null) dbTable = resolveTable(table); // set the corresponding tables among the list of resolved tables: wildcard.setAdqlTable(mapTables.get(dbTable)); }catch(ParseException pe){ errors.addException(pe); } } return mapTables; } /** * Resolve the given table, that's to say search for the corresponding {@link DBTable}. * * @param table The table to resolve. * * @return The corresponding {@link DBTable} if found, <i>null</i> otherwise. * * @throws ParseException An {@link UnresolvedTableException} if the given table can't be resolved. */ protected DBTable resolveTable(final ADQLTable table) throws ParseException{ ArrayList<DBTable> tables = lstTables.search(table); // good if only one table has been found: if (tables.size() == 1) return tables.get(0); // but if more than one: ambiguous table name ! else if (tables.size() > 1) throw new UnresolvedTableException(table, (tables.get(0).getADQLSchemaName() == null ? "" : tables.get(0).getADQLSchemaName() + ".") + tables.get(0).getADQLName(), (tables.get(1).getADQLSchemaName() == null ? "" : tables.get(1).getADQLSchemaName() + ".") + tables.get(1).getADQLName()); // otherwise (no match): unknown table ! else throw new UnresolvedTableException(table); } /** * <p>Search all column references inside the given query, resolve them thanks to the given tables' metadata, * and if there is only one match, attach the matching metadata to them.</p> * * <b>Management of selected columns' references</b> * <p> * A column reference is not only a direct reference to a table column using a column name. * It can also be a reference to an item of the SELECT clause (which will then call a "selected column"). * That kind of reference can be either an index (an unsigned integer starting from 1 to N, where N is the * number selected columns), or the name/alias of the column. * </p> * <p> * These references are also checked, in a second step, in this function. Thus, column metadata are * also attached to them, as common columns. * </p> * * @param query Query in which the existence of tables must be checked. * @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> * @param mapTables List of all resolved tables. * @param list List of column metadata to complete in this function each time a column reference is resolved. * @param errors List of errors to complete in this function each time an unknown table or column is encountered. */ protected void resolveColumns(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final Map<DBTable,ADQLTable> mapTables, final SearchColumnList list, final UnresolvedIdentifiersException errors){ ISearchHandler sHandler; // Check the existence of all columns: sHandler = new SearchColumnOutsideGroupByHandler(); sHandler.search(query); for(ADQLObject result : sHandler){ try{ ADQLColumn adqlColumn = (ADQLColumn)result; // resolve the column: DBColumn dbColumn = resolveColumn(adqlColumn, list, fathersList); // link with the matched DBColumn: adqlColumn.setDBLink(dbColumn); adqlColumn.setAdqlTable(mapTables.get(dbColumn.getTable())); }catch(ParseException pe){ errors.addException(pe); } } // Check the GROUP BY items: ClauseSelect select = query.getSelect(); sHandler = new SearchColumnHandler(); sHandler.search(query.getGroupBy()); for(ADQLObject result : sHandler){ try{ ADQLColumn adqlColumn = (ADQLColumn)result; // resolve the column: DBColumn dbColumn = checkGroupByItem(adqlColumn, select, list); // link with the matched DBColumn: if (dbColumn != null){ adqlColumn.setDBLink(dbColumn); adqlColumn.setAdqlTable(mapTables.get(dbColumn.getTable())); } }catch(ParseException pe){ errors.addException(pe); } } // Check the correctness of all column references (= references to selected columns): /* Note: no need to provide the father tables when resolving column references, * because no father column can be used in ORDER BY. */ sHandler = new SearchColReferenceHandler(); sHandler.search(query); for(ADQLObject result : sHandler){ try{ ColumnReference colRef = (ColumnReference)result; // resolve the column reference: DBColumn dbColumn = checkColumnReference(colRef, select, list); // link with the matched DBColumn: colRef.setDBLink(dbColumn); if (dbColumn != null) colRef.setAdqlTable(mapTables.get(dbColumn.getTable())); }catch(ParseException pe){ errors.addException(pe); } } } /** * <p>Resolve the given column, that's to say search for the corresponding {@link DBColumn}.</p> * * <p> * The third parameter is used only if this function is called inside a sub-query. In this case, * the column is tried to be resolved with the first list (dbColumns). If no match is found, * the resolution is tried with the father columns list (fathersList). * </p> * * @param column The column to resolve. * @param dbColumns List of all available {@link DBColumn}s. * @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> * * @return The corresponding {@link DBColumn} if found. Otherwise an exception is thrown. * * @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved * or an {@link UnresolvedTableException} if its table reference can't be resolved. */ protected DBColumn resolveColumn(final ADQLColumn column, final SearchColumnList dbColumns, Stack<SearchColumnList> fathersList) throws ParseException{ ArrayList<DBColumn> foundColumns = dbColumns.search(column); // good if only one column has been found: if (foundColumns.size() == 1) return foundColumns.get(0); // but if more than one: ambiguous table reference ! else if (foundColumns.size() > 1){ if (column.getTableName() == null) throw new UnresolvedColumnException(column, (foundColumns.get(0).getTable() == null) ? "<NULL>" : (foundColumns.get(0).getTable().getADQLName() + "." + foundColumns.get(0).getADQLName()), (foundColumns.get(1).getTable() == null) ? "<NULL>" : (foundColumns.get(1).getTable().getADQLName() + "." + foundColumns.get(1).getADQLName())); else throw new UnresolvedTableException(column, (foundColumns.get(0).getTable() == null) ? "<NULL>" : foundColumns.get(0).getTable().getADQLName(), (foundColumns.get(1).getTable() == null) ? "<NULL>" : foundColumns.get(1).getTable().getADQLName()); }// otherwise (no match): unknown column ! else{ if (fathersList == null || fathersList.isEmpty()) throw new UnresolvedColumnException(column); else{ Stack<SearchColumnList> subStack = new Stack<SearchColumnList>(); subStack.addAll(fathersList.subList(0, fathersList.size() - 1)); return resolveColumn(column, fathersList.peek(), subStack); } } } /** * Check whether the given column corresponds to a selected item's alias or to an existing column. * * @param col The column to check. * @param select The SELECT clause of the ADQL query. * @param dbColumns The list of all available columns. * * @return The corresponding {@link DBColumn} if this column corresponds to an existing column, * <i>NULL</i> otherwise. * * @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved * or an {@link UnresolvedTableException} if its table reference can't be resolved. * * @see ClauseSelect#searchByAlias(String) * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack) * * @since 1.4 */ protected DBColumn checkGroupByItem(final ADQLColumn col, final ClauseSelect select, final SearchColumnList dbColumns) throws ParseException{ /* If the column name is not qualified, it may be a SELECT-item's alias. * So, try resolving the name as an alias. * If it fails, perform the normal column resolution.*/ if (col.getTableName() == null){ ArrayList<SelectItem> founds = select.searchByAlias(col.getColumnName(), col.isCaseSensitive(IdentifierField.COLUMN)); if (founds.size() == 1) return null; else if (founds.size() > 1) throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias()); } return resolveColumn(col, dbColumns, null); } /** * Check whether the given column reference corresponds to a selected item (column or an expression with an alias) * or to an existing column. * * @param colRef The column reference which must be checked. * @param select The SELECT clause of the ADQL query. * @param dbColumns The list of all available columns. * * @return The corresponding {@link DBColumn} if this reference is actually the name of a column, <i>null</i> otherwise. * * @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved * or an {@link UnresolvedTableException} if its table reference can't be resolved. * * @see ClauseSelect#searchByAlias(String) * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack) */ protected DBColumn checkColumnReference(final ColumnReference colRef, final ClauseSelect select, final SearchColumnList dbColumns) throws ParseException{ if (colRef.isIndex()){ int index = colRef.getColumnIndex(); if (index > 0 && index <= select.size()){ SelectItem item = select.get(index - 1); if (item.getOperand() instanceof ADQLColumn) return ((ADQLColumn)item.getOperand()).getDBLink(); else return null; }else throw new ParseException("Column index out of bounds: " + index + " (must be between 1 and " + select.size() + ") !", colRef.getPosition()); }else{ ADQLColumn col = new ADQLColumn(null, colRef.getColumnName()); col.setCaseSensitive(colRef.isCaseSensitive()); col.setPosition(colRef.getPosition()); // search among the select_item aliases: ArrayList<SelectItem> founds = select.searchByAlias(colRef.getColumnName(), colRef.isCaseSensitive()); if (founds.size() == 1) return null; else if (founds.size() > 1) throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias()); // check the corresponding column: return resolveColumn(col, dbColumns, null); } } /** * Generate a {@link DBTable} corresponding to the given sub-query with the given table name. * This {@link DBTable} will contain all {@link DBColumn} returned by {@link ADQLQuery#getResultingColumns()}. * * @param subQuery Sub-query in which the specified table must be searched. * @param tableName Name of the table to search. * * @return The corresponding {@link DBTable} if the table has been found in the given sub-query, <i>null</i> otherwise. * * @throws ParseException Can be used to explain why the table has not been found. <i>Note: not used by default.</i> */ public static DBTable generateDBTable(final ADQLQuery subQuery, final String tableName) throws ParseException{ DefaultDBTable dbTable = new DefaultDBTable(tableName); DBColumn[] columns = subQuery.getResultingColumns(); for(DBColumn dbCol : columns) dbTable.addColumn(dbCol.copy(dbCol.getADQLName(), dbCol.getADQLName(), dbTable)); return dbTable; } /* ************************* */ /* CHECKING METHODS FOR UDFs */ /* ************************* */ /** * <p>Search all UDFs (User Defined Functions) inside the given query, and then * check their signature against the list of allowed UDFs.</p> * * <p><i>Note: * When more than one allowed function match, the function is considered as correct * and no error is added. * However, in case of multiple matches, the return type of matching functions could * be different and in this case, there would be an error while checking later * the types. In such case, throwing an error could make sense, but the user would * then need to cast some parameters to help the parser identifying the right function. * But the type-casting ability is not yet possible in ADQL. * </i></p> * * @param query Query in which UDFs must be checked. * @param errors List of errors to complete in this function each time a UDF does not match to any of the allowed UDFs. * * @since 1.3 */ protected void checkUDFs(final ADQLQuery query, final UnresolvedIdentifiersException errors){ // 1. Search all UDFs: ISearchHandler sHandler = new SearchUDFHandler(); sHandler.search(query); // If no UDF are allowed, throw immediately an error: if (allowedUdfs.length == 0){ for(ADQLObject result : sHandler) errors.addException(new UnresolvedFunctionException((UserDefinedFunction)result)); } // 2. Try to resolve all of them: else{ ArrayList<UserDefinedFunction> toResolveLater = new ArrayList<UserDefinedFunction>(); UserDefinedFunction udf; int match; BinarySearch<FunctionDef,UserDefinedFunction> binSearch = new BinarySearch<FunctionDef,UserDefinedFunction>(){ @Override protected int compare(UserDefinedFunction searchItem, FunctionDef arrayItem){ return arrayItem.compareTo(searchItem) * -1; } }; // Try to resolve all the found UDFs: /* Note: at this stage, it can happen that UDFs can not be yet resolved because the building of * their signature depends of other UDFs. That's why, these special cases should be kept * for a later resolution try. */ for(ADQLObject result : sHandler){ udf = (UserDefinedFunction)result; // if the type of not all parameters are resolved, postpone the resolution: if (!isAllParamTypesResolved(udf)) toResolveLater.add(udf); // otherwise: else{ // search for a match: match = binSearch.search(udf, allowedUdfs); // if no match... if (match < 0) errors.addException(new UnresolvedFunctionException(udf)); // if there is a match, metadata may be attached (particularly if the function is built automatically by the syntactic parser): else if (udf instanceof DefaultUDF) ((DefaultUDF)udf).setDefinition(allowedUdfs[match]); } } // Try to resolve UDFs whose some parameter types are depending of other UDFs: /* Note: we need to iterate from the end in order to resolve first the most wrapped functions * (e.g. fct1(fct2(...)) ; fct2 must be resolved before fct1). */ for(int i = toResolveLater.size() - 1; i >= 0; i--){ udf = toResolveLater.get(i); // search for a match: match = binSearch.search(udf, allowedUdfs); // if no match, add an error: if (match < 0) errors.addException(new UnresolvedFunctionException(udf)); // otherwise, metadata may be attached (particularly if the function is built automatically by the syntactic parser): else if (udf instanceof DefaultUDF) ((DefaultUDF)udf).setDefinition(allowedUdfs[match]); } // 3. Replace all the resolved DefaultUDF by an instance of the class associated with the set signature: (new ReplaceDefaultUDFHandler(errors)).searchAndReplace(query); } } /** * <p>Tell whether the type of all parameters of the given ADQL function * is resolved.</p> * * <p>A parameter type may not be resolved for 2 main reasons:</p> * <ul> * <li>the parameter is a <b>column</b>, but this column has not been successfully resolved. Thus its type is still unknown.</li> * <li>the parameter is a <b>UDF</b>, but this UDF has not been already resolved. Thus, as for the column, its return type is still unknown. * But it could be known later if the UDF is resolved later ; a second try should be done afterwards.</li> * </ul> * * @param fct ADQL function whose the parameters' type should be checked. * * @return <i>true</i> if the type of all parameters is known, <i>false</i> otherwise. * * @since 1.3 */ protected final boolean isAllParamTypesResolved(final ADQLFunction fct){ for(ADQLOperand op : fct.getParameters()){ if (op.isGeometry() == op.isNumeric() && op.isNumeric() == op.isString()) return false; } return true; } /* ************************************************************************************************* */ /* METHODS CHECKING THE GEOMETRIES (geometrical functions, coordinate systems and STC-S expressions) */ /* ************************************************************************************************* */ /** * <p>Check all geometries.</p> * * <p>Operations done in this function:</p> * <ol> * <li>Check that all geometrical functions are supported</li> * <li>Check that all explicit (string constant) coordinate system definitions are supported</i></li> * <li>Check all STC-S expressions (only in {@link RegionFunction} for the moment) and * Apply the 2 previous checks on them</li> * </ol> * * @param query Query in which geometries must be checked. * @param errors List of errors to complete in this function each time a geometry item is not supported. * * @see #resolveGeometryFunctions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) * @see #resolveCoordinateSystems(ADQLQuery, UnresolvedIdentifiersException) * @see #resolveSTCSExpressions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) * * @since 1.3 */ protected void checkGeometries(final ADQLQuery query, final UnresolvedIdentifiersException errors){ BinarySearch<String,String> binSearch = new BinarySearch<String,String>(){ @Override protected int compare(String searchItem, String arrayItem){ return searchItem.compareToIgnoreCase(arrayItem); } }; // a. Ensure that all used geometry functions are allowed: if (allowedGeo != null) resolveGeometryFunctions(query, binSearch, errors); // b. Check whether the coordinate systems are allowed: if (allowedCoordSys != null) resolveCoordinateSystems(query, errors); // c. Check all STC-S expressions (in RegionFunctions only) + the used coordinate systems (if StringConstant only): if (allowedGeo == null || (allowedGeo.length > 0 && binSearch.search("REGION", allowedGeo) >= 0)) resolveSTCSExpressions(query, binSearch, errors); } /** * Search for all geometrical functions and check whether they are allowed. * * @param query Query in which geometrical functions must be checked. * @param errors List of errors to complete in this function each time a geometrical function is not supported. * * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) * * @since 1.3 */ protected void resolveGeometryFunctions(final ADQLQuery query, final BinarySearch<String,String> binSearch, final UnresolvedIdentifiersException errors){ ISearchHandler sHandler = new SearchGeometryHandler(); sHandler.search(query); String fctName; for(ADQLObject result : sHandler){ fctName = result.getName(); checkGeometryFunction(fctName, (ADQLFunction)result, binSearch, errors); } } /** * <p>Check whether the specified geometrical function is allowed by this implementation.</p> * * <p><i>Note: * If the list of allowed geometrical functions is empty, this function will always add an errors to the given list. * Indeed, it means that no geometrical function is allowed and so that the specified function is automatically not supported. * </i></p> * * @param fctName Name of the geometrical function to test. * @param fct The function instance being or containing the geometrical function to check. <i>Note: this function can be the function to test or a function embedding the function under test (i.e. RegionFunction). * @param binSearch The object to use in order to search a function name inside the list of allowed functions. * It is able to perform a binary search inside a sorted array of String objects. The interest of * this object is its compare function which must be overridden and tells how to compare the item * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). * @param errors List of errors to complete in this function each time a geometrical function is not supported. * * @since 1.3 */ protected void checkGeometryFunction(final String fctName, final ADQLFunction fct, final BinarySearch<String,String> binSearch, final UnresolvedIdentifiersException errors){ int match = -1; if (allowedGeo.length != 0) match = binSearch.search(fctName, allowedGeo); if (match < 0) errors.addException(new UnresolvedFunctionException("The geometrical function \"" + fctName + "\" is not available in this implementation!", fct)); } /** * <p>Search all explicit coordinate system declarations, check their syntax and whether they are allowed by this implementation.</p> * * <p><i>Note: * "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) * * @since 1.3 */ 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(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) * * @since 1.3 */ 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. * * @since 1.3 */ 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())); } } /** * <p>Search 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> * * <p><i>Note: * 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 binSearch The object to use in order to search a region name inside the list of allowed functions/regions. * It is able to perform a binary search inside a sorted array of String objects. The interest of * this object is its compare function which must be overridden and tells how to compare the item * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). * @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(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) * * @since 1.3 */ protected void resolveSTCSExpressions(final ADQLQuery query, final BinarySearch<String,String> binSearch, 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, binSearch, errors); }catch(ParseException pe){ errors.addException(new ParseException(pe.getMessage(), result.getPosition())); } } } /** * <p>Check the given region.</p> * * <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>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(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) * * @since 1.3 */ protected void checkRegion(final Region r, final RegionFunction fct, final BinarySearch<String,String> binSearch, 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: if (allowedGeo != null){ if (allowedGeo.length == 0) errors.addException(new UnresolvedFunctionException("The region type \"" + r.type + "\" is not available in this implementation!", fct)); else checkGeometryFunction((r.type == RegionType.POSITION) ? "POINT" : r.type.toString(), fct, binSearch, errors); } // Check all the inner regions: if (r.regions != null){ for(Region innerR : r.regions) checkRegion(innerR, fct, binSearch, errors); } } /* **************************************************** */ /* METHODS CHECKING TYPES UNKNOWN WHILE CHECKING SYNTAX */ /* **************************************************** */ /** * <p>Search all operands whose the type is not yet known and try to resolve it now * and to check whether it matches the type expected by the syntactic parser.</p> * * <p> * Only two operands may have an unresolved type: columns and user defined functions. * Indeed, their type can be resolved only if the list of available columns and UDFs is known, * and if columns and UDFs used in the query are resolved successfully. * </p> * * <p> * When an operand type is still unknown, they will own the three kinds of type and * so this function won't raise an error: it is thus automatically on the expected type. * This behavior is perfectly correct because if the type is not resolved * that means the item/operand has not been resolved in the previous steps and so that * an error about this item has already been raised. * </p> * * <p><i><b>Important note:</b> * This function does not check the types exactly, but just roughly by considering only three categories: * string, numeric and geometry. * </i></p> * * @param query Query in which unknown types must be resolved and checked. * @param errors List of errors to complete in this function each time a types does not match to the expected one. * * @see UnknownType * * @since 1.3 */ protected void checkTypes(final ADQLQuery query, final UnresolvedIdentifiersException errors){ // Search all unknown types: ISearchHandler sHandler = new SearchUnknownTypeHandler(); sHandler.search(query); // Check whether their type matches the expected one: UnknownType unknown; for(ADQLObject result : sHandler){ unknown = (UnknownType)result; switch(unknown.getExpectedType()){ case 'G': case 'g': if (!unknown.isGeometry()) errors.addException(new ParseException("Type mismatch! A geometry was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); break; case 'N': case 'n': if (!unknown.isNumeric()) errors.addException(new ParseException("Type mismatch! A numeric value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); break; case 'S': case 's': if (!unknown.isString()) errors.addException(new ParseException("Type mismatch! A string value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); break; } } } /* ******************************** */ /* METHODS CHECKING THE SUB-QUERIES */ /* ******************************** */ /** * <p>Search all sub-queries found in the given query but not in the clause FROM. * These sub-queries are then checked using {@link #check(ADQLQuery, Stack)}.</p> * * <b>Fathers stack</b> * <p> * Each time a sub-query must be checked with {@link #check(ADQLQuery, Stack)}, * the list of all columns available in each of its father queries must be provided. * This function is composing itself this stack by adding the given list of available * columns (= all columns resolved in the given query) at the end of the given stack. * If this stack is given empty, then a new stack is created. * </p> * <p> * This modification of the given stack is just the execution time of this function. * Before returning, this function removes the last item of the stack. * </p> * * * @param query Query in which sub-queries must be checked. * @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> * @param availableColumns List of all columns resolved in the given query. * @param errors List of errors to complete in this function each time a semantic error is encountered. * * @since 1.3 */ protected void checkSubQueries(final ADQLQuery query, Stack<SearchColumnList> fathersList, final SearchColumnList availableColumns, final UnresolvedIdentifiersException errors){ // Check sub-queries outside the clause FROM: ISearchHandler sHandler = new SearchSubQueryHandler(); sHandler.search(query); if (sHandler.getNbMatch() > 0){ // Push the list of columns into the father columns stack: if (fathersList == null) fathersList = new Stack<SearchColumnList>(); fathersList.push(availableColumns); // Check each found sub-query: for(ADQLObject result : sHandler){ try{ check((ADQLQuery)result, fathersList); }catch(UnresolvedIdentifiersException uie){ Iterator<ParseException> itPe = uie.getErrors(); while(itPe.hasNext()) errors.addException(itPe.next()); } } // Pop the list of columns from the father columns stack: fathersList.pop(); } } /* *************** */ /* SEARCH HANDLERS */ /* *************** */ /** * Lets searching all {@link ADQLColumn} in the given object, EXCEPT in the GROUP BY clause. * * <p> * {@link ADQLColumn}s of the GROUP BY may be aliases and so, they can not be checked * exactly as a normal column. * </p> * * @author Grégory Mantelet (ARI) * @version 1.4 (05/2017) * @since 1.4 */ private static class SearchColumnOutsideGroupByHandler extends SearchColumnHandler { @Override protected boolean goInto(final ADQLObject obj){ return !(obj instanceof ClauseADQL<?> && ((ClauseADQL<?>)obj).getName() != null && ((ClauseADQL<?>)obj).getName().equalsIgnoreCase("GROUP BY")) && super.goInto(obj); } } /** * Lets searching all tables. * * @author Grégory Mantelet (CDS) * @version 1.0 (07/2011) */ private static class SearchTableHandler extends SimpleSearchHandler { @Override public boolean match(final ADQLObject obj){ return obj instanceof ADQLTable; } } /** * Lets searching all wildcards. * * @author Grégory Mantelet (CDS) * @version 1.0 (09/2011) */ private static class SearchWildCardHandler extends SimpleSearchHandler { @Override public boolean match(final ADQLObject obj){ return (obj instanceof SelectAllColumns) && (((SelectAllColumns)obj).getAdqlTable() != null); } } /** * Lets searching column references. * * @author Grégory Mantelet (CDS) * @version 1.0 (11/2011) */ private static class SearchColReferenceHandler extends SimpleSearchHandler { @Override public boolean match(final ADQLObject obj){ return (obj instanceof ColumnReference); } } /** * <p>Lets searching subqueries in every clause except the FROM one (hence the modification of the {@link #goInto(ADQLObject)}.</p> * * <p><i> * <u>Note:</u> The function {@link #addMatch(ADQLObject, ADQLIterator)} has been modified in order to * not have the root search object (here: the main query) in the list of results. * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.2 (12/2013) * @since 1.2 */ private static class SearchSubQueryHandler extends SimpleSearchHandler { @Override protected void addMatch(ADQLObject matchObj, ADQLIterator it){ if (it != null) super.addMatch(matchObj, it); } @Override protected boolean goInto(ADQLObject obj){ return super.goInto(obj) && !(obj instanceof FromContent); } @Override protected boolean match(ADQLObject obj){ return (obj instanceof ADQLQuery); } } /** * Let searching user defined functions. * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ private static class SearchUDFHandler extends SimpleSearchHandler { @Override protected boolean match(ADQLObject obj){ return (obj instanceof UserDefinedFunction); } } /** * <p>Let replacing every {@link DefaultUDF}s whose a {@link FunctionDef} is set by their corresponding {@link UserDefinedFunction} class.</p> * * <p><i><b>Important note:</b> * If the replacer can not be created using the class returned by {@link FunctionDef#getUDFClass()}, no replacement is performed. * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (02/2015) * @since 1.3 */ private static class ReplaceDefaultUDFHandler extends SimpleReplaceHandler { private final UnresolvedIdentifiersException errors; public ReplaceDefaultUDFHandler(final UnresolvedIdentifiersException errorsContainer){ errors = errorsContainer; } @Override protected boolean match(ADQLObject obj){ return (obj.getClass().getName().equals(DefaultUDF.class.getName())) && (((DefaultUDF)obj).getDefinition() != null) && (((DefaultUDF)obj).getDefinition().getUDFClass() != null); /* Note: detection of DefaultUDF is done on the exact class name rather than using "instanceof" in order to have only direct instances of DefaultUDF, * and not extensions of it. Indeed, DefaultUDFs are generally created automatically by the ADQLQueryFactory ; so, extensions of it can only be custom * UserDefinedFunctions. */ } @Override protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{ try{ // get the associated UDF class: Class<? extends UserDefinedFunction> udfClass = ((DefaultUDF)objToReplace).getDefinition().getUDFClass(); // get the constructor with a single parameter of type ADQLOperand[]: Constructor<? extends UserDefinedFunction> constructor = udfClass.getConstructor(ADQLOperand[].class); // create a new instance of this UDF class with the operands stored in the object to replace: return constructor.newInstance((Object)(((DefaultUDF)objToReplace).getParameters())); /* note: without this class, each item of the given array will be considered as a single parameter. */ }catch(Exception ex){ // IF NO INSTANCE CAN BE CREATED... // ...keep the error for further report: errors.addException(new UnresolvedFunctionException("Impossible to represent the function \"" + ((DefaultUDF)objToReplace).getName() + "\": the following error occured while creating this representation: \"" + ((ex instanceof InvocationTargetException) ? "[" + ex.getCause().getClass().getSimpleName() + "] " + ex.getCause().getMessage() : ex.getMessage()) + "\"", (DefaultUDF)objToReplace)); // ...keep the same object (i.e. no replacement): return objToReplace; } } } /** * Let searching geometrical functions. * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ private static class SearchGeometryHandler extends SimpleSearchHandler { @Override protected boolean match(ADQLObject obj){ return (obj instanceof GeometryFunction); } } /** * <p>Let searching all ADQL objects whose the type was not known while checking the syntax of the ADQL query. * These objects are {@link ADQLColumn}s and {@link UserDefinedFunction}s.</p> * * <p><i><b>Important note:</b> * Only {@link UnknownType} instances having an expected type equals to 'S' (or 's' ; for string) or 'N' (or 'n' ; for numeric) * are kept by this handler. Others are ignored. * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ private static class SearchUnknownTypeHandler extends SimpleSearchHandler { @Override protected boolean match(ADQLObject obj){ if (obj instanceof UnknownType){ char expected = ((UnknownType)obj).getExpectedType(); return (expected == 'G' || expected == 'g' || expected == 'S' || expected == 's' || expected == 'N' || expected == 'n'); }else return false; } } /** * Let searching all explicit declaration of coordinate systems. * So, only {@link StringConstant} objects will be returned. * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ 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 (ARI) * @version 1.3 (10/2014) * @since 1.3 */ 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; } } /** * <p>Implement the binary search algorithm over a sorted array.</p> * * <p> * The only difference with the standard implementation of Java is * that this object lets perform research with a different type * of object than the types of array items. * </p> * * <p> * For that reason, the "compare" function must always be implemented. * </p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * * @param <T> Type of items stored in the array. * @param <S> Type of the item to search. * * @since 1.3 */ protected static abstract class BinarySearch< T, S > { private int s, e, m, comp; /** * <p>Search the given item in the given array.</p> * * <p> * In case the given object matches to several items of the array, * this function will return the smallest index, pointing thus to the first * of all matches. * </p> * * @param searchItem Object for which a corresponding array item must be searched. * @param array Array in which the given object must be searched. * * @return The array index of the first item of all matches. */ public int search(final S searchItem, final T[] array){ s = 0; e = array.length - 1; while(s < e){ // middle of the sorted array: m = s + ((e - s) / 2); // compare the fct with the middle item of the array: comp = compare(searchItem, array[m]); // if the fct is after, trigger the inspection of the right part of the array: if (comp > 0) s = m + 1; // otherwise, the left part: else e = m; } if (s != e || compare(searchItem, array[s]) != 0) return -1; else return s; } /** * Compare the search item and the array item. * * @param searchItem Item whose a corresponding value must be found in the array. * @param arrayItem An item of the array. * * @return Negative value if searchItem is less than arrayItem, 0 if they are equals, or a positive value if searchItem is greater. */ protected abstract int compare(final S searchItem, final T arrayItem); } }