package adql.query; /* * 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 2012-2017 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import java.util.Iterator; import java.util.NoSuchElementException; import adql.db.DBColumn; import adql.db.DBType; import adql.db.DBType.DBDatatype; import adql.db.DefaultDBColumn; import adql.parser.ADQLParser; import adql.parser.ParseException; import adql.query.from.FromContent; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; import adql.query.operand.function.DefaultUDF; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CentroidFunction; import adql.query.operand.function.geometry.CircleFunction; 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; /** * <p>Object representation of an ADQL query or sub-query.</p> * <p>The resulting object of the {@link ADQLParser} is an object of this class.</p> * * @author Grégory Mantelet (CDS;ARI) * @version 1.4 (04/2017) */ public class ADQLQuery implements ADQLObject { /** The ADQL clause SELECT. */ private ClauseSelect select; /** The ADQL clause FROM. */ private FromContent from; /** The ADQL clause WHERE. */ private ClauseConstraints where; /** The ADQL clause GROUP BY. */ private ClauseADQL<ADQLColumn> groupBy; /** The ADQL clause HAVING. */ private ClauseConstraints having; /** The ADQL clause ORDER BY. */ private ClauseADQL<ADQLOrder> orderBy; /** Position of this Query (or sub-query) inside the whole given ADQL query string. * @since 1.4 */ private TextPosition position = null; /** * Builds an empty ADQL query. */ public ADQLQuery(){ select = new ClauseSelect(); from = null; where = new ClauseConstraints("WHERE"); groupBy = new ClauseADQL<ADQLColumn>("GROUP BY"); having = new ClauseConstraints("HAVING"); orderBy = new ClauseADQL<ADQLOrder>("ORDER BY"); } /** * Builds an ADQL query by copying the given one. * * @param toCopy The ADQL query to copy. * @throws Exception If there is an error during the copy. */ @SuppressWarnings("unchecked") public ADQLQuery(ADQLQuery toCopy) throws Exception{ select = (ClauseSelect)toCopy.select.getCopy(); from = (FromContent)toCopy.from.getCopy(); where = (ClauseConstraints)toCopy.where.getCopy(); groupBy = (ClauseADQL<ADQLColumn>)toCopy.groupBy.getCopy(); having = (ClauseConstraints)toCopy.having.getCopy(); orderBy = (ClauseADQL<ADQLOrder>)toCopy.orderBy.getCopy(); position = (toCopy.position == null) ? null : new TextPosition(toCopy.position); } /** * Clear all the clauses. */ public void reset(){ select.clear(); select.setDistinctColumns(false); select.setNoLimit(); from = null; where.clear(); groupBy.clear(); having.clear(); orderBy.clear(); position = null; } /** * Gets the SELECT clause of this query. * * @return Its SELECT clause. */ public final ClauseSelect getSelect(){ return select; } /** * <p>Replaces its SELECT clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newSelect The new SELECT clause. * * @throws NullPointerException If the given SELECT clause is <i>null</i>. */ public void setSelect(ClauseSelect newSelect) throws NullPointerException{ if (newSelect == null) throw new NullPointerException("Impossible to replace the SELECT clause of a query by NULL !"); else select = newSelect; position = null; } /** * Gets the FROM clause of this query. * * @return Its FROM clause. */ public final FromContent getFrom(){ return from; } /** * <p>Replaces its FROM clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newFrom The new FROM clause. * * @throws NullPointerException If the given FROM clause is <i>null</i>. */ public void setFrom(FromContent newFrom) throws NullPointerException{ if (newFrom == null) throw new NullPointerException("Impossible to replace the FROM clause of a query by NULL !"); else from = newFrom; position = null; } /** * Gets the WHERE clause of this query. * * @return Its WHERE clause. */ public final ClauseConstraints getWhere(){ return where; } /** * <p>Replaces its WHERE clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newWhere The new WHERE clause. * * @throws NullPointerException If the given WHERE clause is <i>null</i>. */ public void setWhere(ClauseConstraints newWhere) throws NullPointerException{ if (newWhere == null) where.clear(); else where = newWhere; position = null; } /** * Gets the GROUP BY clause of this query. * * @return Its GROUP BY clause. */ public final ClauseADQL<ADQLColumn> getGroupBy(){ return groupBy; } /** * <p>Replaces its GROUP BY clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newGroupBy The new GROUP BY clause. * @throws NullPointerException If the given GROUP BY clause is <i>null</i>. */ public void setGroupBy(ClauseADQL<ADQLColumn> newGroupBy) throws NullPointerException{ if (newGroupBy == null) groupBy.clear(); else groupBy = newGroupBy; position = null; } /** * Gets the HAVING clause of this query. * * @return Its HAVING clause. */ public final ClauseConstraints getHaving(){ return having; } /** * <p>Replaces its HAVING clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newHaving The new HAVING clause. * @throws NullPointerException If the given HAVING clause is <i>null</i>. */ public void setHaving(ClauseConstraints newHaving) throws NullPointerException{ if (newHaving == null) having.clear(); else having = newHaving; position = null; } /** * Gets the ORDER BY clause of this query. * * @return Its ORDER BY clause. */ public final ClauseADQL<ADQLOrder> getOrderBy(){ return orderBy; } /** * <p>Replaces its ORDER BY clause by the given one.</p> * * <p><i>note: the position of the query is erased.</i></p> * * @param newOrderBy The new ORDER BY clause. * @throws NullPointerException If the given ORDER BY clause is <i>null</i>. */ public void setOrderBy(ClauseADQL<ADQLOrder> newOrderBy) throws NullPointerException{ if (newOrderBy == null) orderBy.clear(); else orderBy = newOrderBy; position = null; } @Override public final TextPosition getPosition(){ return position; } /** * Set the position of this {@link ADQLQuery} (or sub-query) inside the whole given ADQL query string. * * @param position New position of this {@link ADQLQuery}. * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } @Override public ADQLObject getCopy() throws Exception{ return new ADQLQuery(this); } @Override public String getName(){ return "{ADQL query}"; } /** * <p>Gets the list of columns (database metadata) selected by this query.</p> * * <p><i><u>Note:</u> The list is generated on the fly !</i></p> * * @return Selected columns metadata. */ public DBColumn[] getResultingColumns(){ ArrayList<DBColumn> columns = new ArrayList<DBColumn>(select.size()); for(SelectItem item : select){ ADQLOperand operand = item.getOperand(); if (item instanceof SelectAllColumns){ try{ // If "{table}.*", add all columns of the specified table: if (((SelectAllColumns)item).getAdqlTable() != null) columns.addAll(((SelectAllColumns)item).getAdqlTable().getDBColumns()); // Otherwise ("*"), add all columns of all selected tables: else columns.addAll(from.getDBColumns()); }catch(ParseException pe){ // Here, this error should not occur any more, since it must have been caught by the DBChecker! } }else{ // Create the DBColumn: DBColumn col = null; // ...whose the name will be set with the SELECT item's alias: if (item.hasAlias()){ if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null){ col = ((ADQLColumn)operand).getDBLink(); col = col.copy(col.getDBName(), item.getAlias(), col.getTable()); }else col = new DefaultDBColumn(item.getAlias(), null); } // ...or whose the name will be the name of the SELECT item: else{ if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null) col = ((ADQLColumn)operand).getDBLink(); else col = new DefaultDBColumn(item.getName(), null); } /* For columns created by default (from functions and operations generally), * set the adequate type if known: */ // CASE: Well-defined UDF if (operand instanceof DefaultUDF && ((DefaultUDF)operand).getDefinition() != null){ DBType type = ((DefaultUDF)operand).getDefinition().returnType; ((DefaultDBColumn)col).setDatatype(type); } // CASE: Point type: else if (operand instanceof PointFunction || operand instanceof CentroidFunction) ((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.POINT)); // CASE: Region type: else if (operand instanceof RegionFunction || operand instanceof CircleFunction || operand instanceof BoxFunction || operand instanceof PolygonFunction) ((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.REGION)); // CASE: String and numeric types else if (col instanceof DefaultDBColumn && col.getDatatype() == null && operand.isNumeric() != operand.isString()){ // CASE: String types if (operand.isString()) ((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.VARCHAR)); // CASE: Numeric types: /* Note: a little special case here since a numeric could be a real, double, integer, or anything * else and that we don't know precisely here. So we set the special UNKNOWN NUMERIC type. */ else ((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.UNKNOWN_NUMERIC)); } // Add the new column to the list: columns.add(col); } } DBColumn[] resColumns = new DBColumn[columns.size()]; return columns.toArray(resColumns); } /** * Lets searching ADQL objects into this ADQL query thanks to the given search handler. * * @param sHandler A search handler. * * @return An iterator on all ADQL objects found. */ public Iterator<ADQLObject> search(ISearchHandler sHandler){ sHandler.search(this); return sHandler.iterator(); } @Override public ADQLIterator adqlIterator(){ return new ADQLIterator(){ private int index = -1; private ClauseADQL<?> currentClause = null; @Override public ADQLObject next(){ index++; switch(index){ case 0: currentClause = select; break; case 1: currentClause = null; return from; case 2: currentClause = where; break; case 3: currentClause = groupBy; break; case 4: currentClause = having; break; case 5: currentClause = orderBy; break; default: throw new NoSuchElementException(); } return currentClause; } @Override public boolean hasNext(){ return index + 1 < 6; } @Override @SuppressWarnings("unchecked") public void replace(ADQLObject replacer) throws UnsupportedOperationException, IllegalStateException{ if (index <= -1) throw new IllegalStateException("replace(ADQLObject) impossible: next() has not yet been called !"); if (replacer == null) remove(); else{ switch(index){ case 0: if (replacer instanceof ClauseSelect) select = (ClauseSelect)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseSelect (" + select.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; case 1: if (replacer instanceof FromContent) from = (FromContent)replacer; else throw new UnsupportedOperationException("Impossible to replace a FromContent (" + from.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; case 2: if (replacer instanceof ClauseConstraints) where = (ClauseConstraints)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseConstraints (" + where.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; case 3: if (replacer instanceof ClauseADQL) groupBy = (ClauseADQL<ADQLColumn>)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseADQL (" + groupBy.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; case 4: if (replacer instanceof ClauseConstraints) having = (ClauseConstraints)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseConstraints (" + having.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; case 5: if (replacer instanceof ClauseADQL) orderBy = (ClauseADQL<ADQLOrder>)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseADQL (" + orderBy.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); break; } position = null; } } @Override public void remove(){ if (index <= -1) throw new IllegalStateException("remove() impossible: next() has not yet been called !"); if (index == 0 || index == 1) throw new UnsupportedOperationException("Impossible to remove a " + ((index == 0) ? "SELECT" : "FROM") + " clause from a query !"); else{ currentClause.clear(); position = null; } } }; } @Override public String toADQL(){ StringBuffer adql = new StringBuffer(select.toADQL()); adql.append("\nFROM ").append(from.toADQL()); if (!where.isEmpty()) adql.append('\n').append(where.toADQL()); if (!groupBy.isEmpty()) adql.append('\n').append(groupBy.toADQL()); if (!having.isEmpty()) adql.append('\n').append(having.toADQL()); if (!orderBy.isEmpty()) adql.append('\n').append(orderBy.toADQL()); return adql.toString(); } }