package adql.query.from; /* * 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-2016 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import adql.db.DBColumn; import adql.db.DBCommonColumn; import adql.db.SearchColumnList; import adql.db.exception.UnresolvedJoinException; import adql.query.ADQLIterator; import adql.query.ADQLObject; import adql.query.ClauseConstraints; import adql.query.IdentifierField; import adql.query.TextPosition; import adql.query.operand.ADQLColumn; /** * Defines a join between two "tables". * * @author Grégory Mantelet (CDS;ARI) * @version 1.4 (03/2016) */ public abstract class ADQLJoin implements ADQLObject, FromContent { /** The left "table" of this join. */ private FromContent leftTable; /** The right "table" of this join. */ private FromContent rightTable; /** Natural join (use of table keys) ? */ protected boolean natural = false; /** The join condition. */ protected ClauseConstraints condition = null; /** List of columns on which the join must be done. */ protected ArrayList<ADQLColumn> lstColumns = null; /** Position of this {@link ADQLJoin} in the given ADQL query string. * @since 1.4 */ private TextPosition position = null; /* ************ */ /* CONSTRUCTORS */ /* ************ */ /** * Builds an {@link ADQLJoin} with at least two {@link FromContent} objects: * the left and the right part of the join (usually two tables: T1 JOIN T2). * * @param left Left "table" of the join. * @param right Right "table" of the join. */ public ADQLJoin(FromContent left, FromContent right){ leftTable = left; rightTable = right; } /** * Builds an ADQL join by copying the given one. * * @param toCopy The ADQLJoin to copy. * * @throws Exception If there is an error during the copy. */ public ADQLJoin(ADQLJoin toCopy) throws Exception{ leftTable = (FromContent)(toCopy.leftTable.getCopy()); rightTable = (FromContent)(toCopy.rightTable.getCopy()); natural = toCopy.natural; condition = (ClauseConstraints)(toCopy.condition.getCopy()); if (toCopy.lstColumns != null){ lstColumns = new ArrayList<ADQLColumn>(toCopy.lstColumns.size()); for(ADQLColumn col : toCopy.lstColumns) lstColumns.add((ADQLColumn)col.getCopy()); } position = (toCopy.position == null) ? null : new TextPosition(toCopy.position); } /* ***************** */ /* GETTERS & SETTERS */ /* ***************** */ /** * Indicates whether this join is natural or not. * * @return <i>true</i> means this join is natural, <i>false</i> else. */ public final boolean isNatural(){ return natural; } /** * Lets indicate that this join is natural (it must use the table keys). * * @param natural <i>true</i> means this join must be natural, <i>false</i> else. */ public void setNatural(boolean natural){ this.natural = natural; if (natural){ condition = null; lstColumns = null; } position = null; } /** * Gets the left "table" of this join. * * @return The left part of the join. */ public final FromContent getLeftTable(){ return leftTable; } /** * Sets the left "table" of this join. * * @param table The left part of the join. */ public void setLeftTable(FromContent table){ leftTable = table; position = null; } /** * Gets the right "table" of this join. * * @return The right part of the join. */ public final FromContent getRightTable(){ return rightTable; } /** * Sets the right "table" of this join. * * @param table The right part of the join. */ public void setRightTable(FromContent table){ rightTable = table; position = null; } /** * Gets the condition of this join (that's to say: the condition which follows the keyword ON). * * @return The join condition. */ public final ClauseConstraints getJoinCondition(){ return condition; } /** * Sets the condition of this join (that's to say: the condition which follows the keyword ON). * * @param cond The join condition (condition following ON). */ public void setJoinCondition(ClauseConstraints cond){ condition = cond; if (condition != null){ natural = false; lstColumns = null; } position = null; } @Override public final TextPosition getPosition(){ return position; } @Override public final void setPosition(final TextPosition position){ this.position = position; } /** * Gets the list of all columns on which the join is done (that's to say: the list of columns given with the keyword USING). * * @return The joined columns (columns listed in USING(...)). */ public final Iterator<ADQLColumn> getJoinedColumns(){ if (lstColumns == null){ return new Iterator<ADQLColumn>(){ @Override public boolean hasNext(){ return false; } @Override public ADQLColumn next(){ return null; } @Override public void remove(){ ; } }; }else return lstColumns.iterator(); } /** * Tells whether this join has a list of columns to join. * * @return <i>true</i> if some columns must be explicitly joined, <i>false</i> otherwise. */ public final boolean hasJoinedColumns(){ return (lstColumns != null); } /** * Sets the list of all columns on which the join is done (that's to say: the list of columns given with the keyword USING). * * @param columns The joined columns. */ public void setJoinedColumns(Collection<ADQLColumn> columns){ if (columns != null && !columns.isEmpty()){ if (lstColumns == null) lstColumns = new ArrayList<ADQLColumn>(columns.size()); else lstColumns.clear(); lstColumns.addAll(columns); natural = false; condition = null; position = null; } } /* ***************** */ /* INHERITED METHODS */ /* ***************** */ @Override public String getName(){ return getJoinType(); } @Override public ADQLIterator adqlIterator(){ return new ADQLIterator(){ private int index = -1; private final int nbItems = 2 + ((condition == null) ? 0 : 1) + ((lstColumns == null) ? 0 : lstColumns.size()); private final int offset = 2 + ((condition == null) ? 0 : 1); private Iterator<ADQLColumn> itCol = null; @Override public ADQLObject next(){ index++; if (index == 0) return leftTable; else if (index == 1) return rightTable; else if (index == 2 && condition != null) return condition; else if (lstColumns != null && !lstColumns.isEmpty()){ if (itCol == null) itCol = lstColumns.iterator(); return itCol.next(); }else throw new NoSuchElementException(); } @Override public boolean hasNext(){ return (itCol != null && itCol.hasNext()) || index + 1 < nbItems; } @Override 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 if (index == 0){ if (replacer instanceof FromContent) leftTable = (FromContent)replacer; else throw new UnsupportedOperationException("Impossible to replace the left \"table\" of the join (" + leftTable.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") ! The replacer must be a FromContent instance."); }else if (index == 1){ if (replacer instanceof FromContent) rightTable = (FromContent)replacer; else throw new UnsupportedOperationException("Impossible to replace the right \"table\" of the join (" + rightTable.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") ! The replacer must be a FromContent instance."); }else if (index == 2 && itCol == null){ if (replacer instanceof ClauseConstraints) condition = (ClauseConstraints)replacer; else throw new UnsupportedOperationException("Impossible to replace an ADQLConstraint (" + condition + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); }else if (itCol != null){ if (replacer instanceof ADQLColumn) lstColumns.set(index - offset, (ADQLColumn)replacer); else throw new UnsupportedOperationException("Impossible to replace an ADQLColumn by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !"); } position = null; } @Override public void remove(){ if (index <= -1) throw new IllegalStateException("remove() impossible: next() has not yet been called !"); else if (index == 0) throw new UnsupportedOperationException("Impossible to remove the left \"table\" of the join (" + leftTable.toADQL() + ") !"); else if (index == 1) throw new UnsupportedOperationException("Impossible to remove the right \"table\" of the join (" + rightTable.toADQL() + ") !"); else if (index == 2 && itCol == null) throw new UnsupportedOperationException("Impossible to remove a condition (" + condition.toADQL() + ") from a join (" + toADQL() + ") !"); else if (itCol != null){ itCol.remove(); index--; position = null; } } }; } @Override public String toADQL(){ StringBuffer adql = new StringBuffer(leftTable.toADQL()); adql.append(natural ? " NATURAL " : " ").append(getJoinType()).append(' ').append(rightTable.toADQL()); if (condition != null) adql.append(" ON ").append(condition.toADQL()); else if (lstColumns != null){ String cols = null; for(ADQLColumn item : lstColumns){ cols = (cols == null) ? ("\"" + item.toADQL() + "\"") : (cols + ", \"" + item.toADQL() + "\""); } adql.append(" USING (").append(cols).append(')'); } return adql.toString(); } @Override public SearchColumnList getDBColumns() throws UnresolvedJoinException{ try{ SearchColumnList list = new SearchColumnList(); SearchColumnList leftList = leftTable.getDBColumns(); SearchColumnList rightList = rightTable.getDBColumns(); /* 1. Figure out duplicated columns */ HashMap<String,DBCommonColumn> mapDuplicated = new HashMap<String,DBCommonColumn>(); // CASE: NATURAL if (natural){ // Find duplicated items between the two lists and add one common column in mapDuplicated for each DBColumn rightCol; for(DBColumn leftCol : leftList){ // search for at most one column with the same name in the RIGHT list // and throw an exception is there are several matches: rightCol = findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false); // if there is one... if (rightCol != null){ // ...check there is only one column with this name in the LEFT list, // and throw an exception if it is not the case: findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true); // ...create a common column: mapDuplicated.put(leftCol.getADQLName().toLowerCase(), new DBCommonColumn(leftCol, rightCol)); } } } // CASE: USING else if (lstColumns != null && !lstColumns.isEmpty()){ // For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated DBColumn leftCol, rightCol; for(ADQLColumn usingCol : lstColumns){ // search for exactly one column with the same name in the LEFT list // and throw an exception if there is none, or if there are several matches: leftCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true); // idem in the RIGHT list: rightCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); // create a common column: mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), new DBCommonColumn(leftCol, rightCol)); } } // CASE: NO DUPLICATION TO FIGURE OUT else{ // Return the union of both lists: list.addAll(leftList); list.addAll(rightList); return list; } /* 2. Add all columns of the left list except the ones identified as duplications */ addAllExcept(leftList, list, mapDuplicated); /* 3. Add all columns of the right list except the ones identified as duplications */ addAllExcept(rightList, list, mapDuplicated); /* 4. Add all common columns of mapDuplicated */ list.addAll(0, mapDuplicated.values()); return list; }catch(UnresolvedJoinException uje){ uje.setPosition(position); throw uje; } } public final static void addAllExcept(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map<String,DBCommonColumn> exception){ for(DBColumn col : itemsToAdd){ if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\"")) target.add(col); } } public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoinException{ DBColumn result = findAtMostOneColumn(columnName, caseSensitive, list, leftList); if (result == null) throw new UnresolvedJoinException("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); else return result; } public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoinException{ ArrayList<DBColumn> result = list.search(null, null, null, columnName, caseSensitive); if (result.isEmpty()) return null; else if (result.size() > 1) throw new UnresolvedJoinException("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); else return result.get(0); } /** * Tells whether the given column is a common column (that's to say, a unification of several columns of the same name). * * @param col A DBColumn. * @return true if the given column is a common column, false otherwise (particularly if col = null). */ public static final boolean isCommonColumn(final DBColumn col){ return (col != null && col instanceof DBCommonColumn); } @Override public ArrayList<ADQLTable> getTables(){ ArrayList<ADQLTable> tables = leftTable.getTables(); tables.addAll(rightTable.getTables()); return tables; } @Override public ArrayList<ADQLTable> getTablesByAlias(final String alias, final boolean caseSensitive){ ArrayList<ADQLTable> tables = leftTable.getTablesByAlias(alias, caseSensitive); tables.addAll(rightTable.getTablesByAlias(alias, caseSensitive)); return tables; } /* **************** */ /* ABSTRACT METHODS */ /* **************** */ /** * Gets the type of this join. * * @return Its join type (i.e. CROSS JOIN, LEFT JOIN, LEFT OUTER JOIN, ...). */ public abstract String getJoinType(); @Override public abstract ADQLObject getCopy() throws Exception; }