/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.servoy.base.query.BaseQueryTable;
import com.servoy.j2db.query.QueryFunction.QueryFunctionType;
import com.servoy.j2db.util.serialize.ReplacedObject;
import com.servoy.j2db.util.visitor.IVisitor;
import com.servoy.j2db.util.visitor.ObjectCountVisitor;
/**
* Query select statement.
* @author rgansevles
*/
public final class QuerySelect extends AbstractBaseQuery implements ISQLSelect
{
private static final String[] EMPTY_STRINGS = new String[0];
private BaseQueryTable table;
private ArrayList<IQuerySelectValue> columns; // declare as ArrayList in stead of List -> must be sure that it is Serializable
private boolean distinct = false;
private boolean plain_pk_select = false;
private HashMap<String, AndCondition> conditions = null; // Map of AndCondition objects
private ArrayList<ISQLJoin> joins;
private ArrayList<IQuerySort> sorts;
private ArrayList<IQuerySelectValue> groupBy;
private AndCondition having = null;
private int lockMode = LOCK_MODE_NONE;
private static final long serialVersionUID = 1L;
public QuerySelect(BaseQueryTable table)
{
this.table = table;
}
public ArrayList<IQuerySelectValue> getColumns()
{
return columns;
}
public String[] getColumnNames()
{
List<String> names = new ArrayList<String>();
if (getColumns() != null)
{
for (IQuerySelectValue column : getColumns())
{
names.add(column.getColumn().getName());
}
}
return names.toArray(new String[names.size()]);
}
public void addColumn(IQuerySelectValue c)
{
if (c == null)
{
return;
}
if (columns == null)
{
columns = new ArrayList<IQuerySelectValue>();
}
columns.add(c);
}
public void setTable(BaseQueryTable table)
{
this.table = table;
}
public void setColumns(ArrayList<IQuerySelectValue> cols)
{
int i;
for (i = 0; cols != null && i < cols.size(); i++)
{
Object col = cols.get(i);
if (!(col instanceof IQuerySelectValue))
{
throw new IllegalArgumentException("Unknown column class " + col.getClass().getName()); //$NON-NLS-1$
}
}
columns = i == 0 ? null : cols;
}
public void setJoins(ArrayList<ISQLJoin> jns)
{
int i;
for (i = 0; jns != null && i < jns.size(); i++)
{
Object join = jns.get(i);
if (!(join instanceof ISQLJoin))
{
throw new IllegalArgumentException("Unknown join class " + join.getClass().getName()); //$NON-NLS-1$
}
}
joins = i == 0 ? null : jns;
}
public void setSorts(ArrayList< ? extends IQuerySort> srts)
{
if (srts != null && srts.size() > 0)
{
sorts = new ArrayList<IQuerySort>(srts.size());
for (int i = 0; i < srts.size(); i++)
{
Object sort = srts.get(i);
if (!(sort instanceof IQuerySort))
{
sorts = null;
throw new IllegalArgumentException("Unknown sort class " + sort.getClass().getName()); //$NON-NLS-1$
}
sorts.add((IQuerySort)sort);
}
}
else
{
sorts = null;
}
}
public void setGroupBy(ArrayList< ? extends IQuerySelectValue> gb)
{
int i;
for (i = 0; gb != null && i < gb.size(); i++)
{
Object sort = gb.get(i);
if (!(sort instanceof QueryColumn))
{
throw new IllegalArgumentException("Unknown group by class " + sort.getClass().getName()); //$NON-NLS-1$
}
}
groupBy = i == 0 ? null : new ArrayList<IQuerySelectValue>(gb);
}
public void setCondition(String name, ISQLCondition c)
{
conditions = setInConditionMap(conditions, name, c);
}
public void addCondition(String name, ISQLCondition c)
{
conditions = addToConditionMap(conditions, name, c);
}
/**
* Add a condition that 'OR's with the existing condition.
*
* @param name condition name
* @param c condition
*/
public void addConditionOr(String name, ISQLCondition c)
{
if (c == null)
{
return;
}
AndCondition condition = null;
if (conditions != null)
{
condition = conditions.get(name);
}
if (condition == null)
{
// nothing to OR against
addCondition(name, c);
}
else
{
List<ISQLCondition> clauses = condition.getConditions();
if (clauses.size() == 1 && clauses.get(0) instanceof OrCondition)
{
((OrCondition)clauses.get(0)).addCondition(c);
}
else if (clauses.size() > 0 && BooleanCondition.FALSE_CONDITION.equals(clauses.get(0)))
{
// c OR FALSE is equivalent to c
clauses.set(0, c);
}
else
{
// note that the values of the conditions map are always of type AndCondition
AndCondition newCondition = new AndCondition();
OrCondition or = new OrCondition();
or.addCondition(condition);
or.addCondition(c);
newCondition.addCondition(or);
conditions.put(name, newCondition);
}
}
}
public void clearConditions()
{
conditions = null;
}
public void setHaving(ISQLCondition c)
{
if (c == null || c instanceof AndCondition)
{
having = (AndCondition)c;
}
else
{
having = new AndCondition();
having.addCondition(c);
}
}
public void addHaving(ISQLCondition c)
{
if (having == null)
{
setHaving(c);
}
else if (c != null)
{
having.addCondition(c);
}
}
public void clearJoins()
{
joins = null;
}
public void clearCondition(String name)
{
setCondition(name, null);
}
public void clearHaving()
{
having = null;
}
public void clearSorts()
{
sorts = null;
}
public void clearGroupBy()
{
groupBy = null;
}
public void addJoin(ISQLJoin join)
{
if (join == null)
{
return;
}
if (joins == null)
{
joins = new ArrayList<ISQLJoin>();
}
joins.add(join);
}
public void addSort(IQuerySort s)
{
if (s == null)
{
return;
}
if (sorts == null)
{
sorts = new ArrayList<IQuerySort>();
}
if (!sorts.contains(s)) sorts.add(s);
}
public void addGroupBy(IQuerySelectValue c)
{
if (c == null)
{
return;
}
if (groupBy == null)
{
groupBy = new ArrayList<IQuerySelectValue>();
}
groupBy.add(c);
}
public void setDistinct(boolean d)
{
distinct = d;
}
public void setPlainPKSelect(boolean p)
{
plain_pk_select = p;
}
public void setLockMode(int lockMode)
{
this.lockMode = lockMode;
}
public AndCondition getCondition(String name)
{
if (conditions == null)
{
return null;
}
return conditions.get(name);
}
public String[] getConditionNames()
{
if (conditions == null)
{
return EMPTY_STRINGS;
}
return conditions.keySet().toArray(EMPTY_STRINGS);
}
public AndCondition getConditionClone(String name)
{
return AbstractBaseQuery.deepClone(getCondition(name));
}
public boolean isDistinct()
{
return distinct;
}
public boolean isUnique()
{
return distinct || (plain_pk_select && joins == null);
}
public ArrayList<ISQLJoin> getJoins()
{
return joins;
}
public ISQLJoin getJoin(BaseQueryTable primaryTable, String name)
{
for (int i = 0; joins != null && i < joins.size(); i++)
{
ISQLJoin join = joins.get(i);
if (name.equals(join.getName()) && primaryTable.equals(join.getPrimaryTable()))
{
return join;
}
}
return null;
}
public ArrayList<ISQLJoin> getJoinsClone()
{
return AbstractBaseQuery.deepClone(joins);
}
public ArrayList<IQuerySelectValue> getColumnsClone()
{
return AbstractBaseQuery.deepClone(columns);
}
public ArrayList<IQuerySelectValue> getGroupByClone()
{
return AbstractBaseQuery.deepClone(groupBy);
}
public ArrayList<IQuerySort> getSortsClone()
{
return AbstractBaseQuery.deepClone(sorts);
}
public ArrayList<IQuerySort> getSorts()
{
return sorts;
}
public ArrayList<IQuerySelectValue> getGroupBy()
{
return groupBy;
}
public BaseQueryTable getTable()
{
return table;
}
public int getLockMode()
{
return lockMode;
}
public String getLockModeString()
{
switch (lockMode)
{
case LOCK_MODE_NONE :
return "NONE"; //$NON-NLS-1$
case LOCK_MODE_LOCK_BLOCK :
return "BLOCK"; //$NON-NLS-1$
case LOCK_MODE_LOCK_NOWAIT :
return "NOWAIT"; //$NON-NLS-1$
default :
return "!UKNOWN!"; //$NON-NLS-1$
}
}
/**
* Is any condition set?
*
* @return
*/
public boolean hasAnyCondition()
{
return conditions != null || having != null || joins != null;
}
/**
* One condition to rule them all. Does not include having-conditions.
*
* @return
*/
public AndCondition getWhere()
{
ISQLCondition where = getConditionMapCondition(conditions);
if (where == null || where instanceof AndCondition)
{
return (AndCondition)where;
}
AndCondition and = new AndCondition();
and.addCondition(where);
return and;
}
public Map<String, AndCondition> getConditions()
{
return conditions;
}
static ISQLCondition getConditionMapCondition(Map<String, AndCondition> conditions)
{
if (conditions == null)
{
return null;
}
if (conditions.size() == 1)
{
return conditions.values().iterator().next();
}
AndCondition where = new AndCondition();
Iterator<AndCondition> it = conditions.values().iterator();
while (it.hasNext())
{
where.addCondition(it.next());
}
return where;
}
public ISQLCondition getWhereClone()
{
return AbstractBaseQuery.deepClone(getWhere());
}
/**
* All having-conditions combined.
*
* @return
*/
public AndCondition getHaving()
{
return having;
}
public ISQLCondition getHavingClone()
{
return AbstractBaseQuery.deepClone(getHaving());
}
@Override
public Object shallowClone() throws CloneNotSupportedException
{
return super.clone();
}
public QuerySelect getSelectCount(String name, boolean distinctCount)
{
QuerySelect selectCount = AbstractBaseQuery.deepClone(this);
selectCount.clearSorts();
// remove unused joins
selectCount.removeUnusedJoins(true);
IQuerySelectValue agregee;
if (selectCount.joins != null && (selectCount.distinct || distinctCount))
{
ArrayList<IQuerySelectValue> selectColumns = selectCount.getColumns();
IQuerySelectValue[] cols = selectColumns.toArray(new IQuerySelectValue[selectColumns.size()]);
if (cols.length == 1)
{
agregee = new QueryFunction(QueryFunctionType.distinct, cols[0], null);
}
else
{
agregee = new QueryFunction(QueryFunctionType.distinct, new QueryFunction(QueryFunctionType.concat, cols, null), null);
}
}
else
{
agregee = new QueryColumnValue("*", null, true);
}
ArrayList<IQuerySelectValue> countCol = new ArrayList<IQuerySelectValue>();
countCol.add(new QueryAggregate(QueryAggregate.COUNT, agregee, name));
selectCount.setColumns(countCol);
return selectCount;
}
/**
* Remove joins that whose foreign table is not referred to in this query.
*/
public void removeUnusedJoins(boolean keepInnerjoins)
{
boolean updated = true;
while (joins != null && updated)
{
updated = false;
int njoins = joins.size();
for (int i = 0; i < njoins && !updated; i++)
{
ISQLJoin join = joins.get(i);
if (!(join instanceof ISQLTableJoin) || (keepInnerjoins && ((ISQLTableJoin)join).hasInnerJoin()))
{
// count may depend on related records
continue;
}
BaseQueryTable joinTable = ((ISQLTableJoin)join).getForeignTable();
ObjectCountVisitor selectCounter = new ObjectCountVisitor(joinTable, true);
ObjectCountVisitor joinCounter = new ObjectCountVisitor(joinTable, true);
acceptVisitor(selectCounter);
join.acceptVisitor(joinCounter);
if (selectCounter.getCount() == joinCounter.getCount())
{
// the table is not referenced outside the join; it may be removed
if (njoins == 1)
{
joins = null;
}
else
{
joins.remove(i);
}
updated = true;
}
}
}
}
static HashMap<String, AndCondition> setInConditionMap(HashMap<String, AndCondition> map, String name, ISQLCondition c)
{
HashMap<String, AndCondition> retval = map;
if (c == null)
{
if (retval != null)
{
retval.remove(name);
if (retval.size() == 0)
{
return null;
}
}
}
else
{
AndCondition condition;
if (c instanceof AndCondition)
{
condition = (AndCondition)c;
}
else
{
condition = new AndCondition();
condition.addCondition(c);
}
if (retval == null)
{
retval = new HashMap<String, AndCondition>();
}
retval.put(name, condition);
}
return retval;
}
static HashMap<String, AndCondition> addToConditionMap(HashMap<String, AndCondition> map, String name, ISQLCondition c)
{
HashMap<String, AndCondition> retval = map;
if (c != null)
{
AndCondition condition = null;
if (retval == null)
{
retval = new HashMap<String, AndCondition>();
}
else
{
condition = retval.get(name);
}
if (condition == null)
{
condition = new AndCondition();
retval.put(name, condition);
}
condition.addCondition(c);
}
return retval;
}
public void acceptVisitor(IVisitor visitor)
{
table = AbstractBaseQuery.acceptVisitor(table, visitor);
columns = AbstractBaseQuery.acceptVisitor(columns, visitor);
conditions = AbstractBaseQuery.acceptVisitor(conditions, visitor);
having = AbstractBaseQuery.acceptVisitor(having, visitor);
joins = AbstractBaseQuery.acceptVisitor(joins, visitor);
sorts = AbstractBaseQuery.acceptVisitor(sorts, visitor);
groupBy = AbstractBaseQuery.acceptVisitor(groupBy, visitor);
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((columns == null) ? 0 : columns.hashCode());
result = prime * result + ((conditions == null) ? 0 : conditions.hashCode());
result = prime * result + ((having == null) ? 0 : having.hashCode());
result = prime * result + (distinct ? 1231 : 1237);
result = prime * result + ((groupBy == null) ? 0 : groupBy.hashCode());
result = prime * result + ((joins == null) ? 0 : joins.hashCode());
result = prime * result + lockMode;
result = prime * result + (plain_pk_select ? 1231 : 1237);
result = prime * result + ((sorts == null) ? 0 : sorts.hashCode());
result = prime * result + ((table == null) ? 0 : table.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final QuerySelect other = (QuerySelect)obj;
if (columns == null)
{
if (other.columns != null) return false;
}
else if (!columns.equals(other.columns)) return false;
if (conditions == null)
{
if (other.conditions != null) return false;
}
else if (!conditions.equals(other.conditions)) return false;
if (having == null)
{
if (other.having != null) return false;
}
else if (!having.equals(other.having)) return false;
if (distinct != other.distinct) return false;
if (groupBy == null)
{
if (other.groupBy != null) return false;
}
else if (!groupBy.equals(other.groupBy)) return false;
if (joins == null)
{
if (other.joins != null) return false;
}
else if (!joins.equals(other.joins)) return false;
if (lockMode != other.lockMode) return false;
if (plain_pk_select != other.plain_pk_select) return false;
if (sorts == null)
{
if (other.sorts != null) return false;
}
else if (!sorts.equals(other.sorts)) return false;
if (table == null)
{
if (other.table != null) return false;
}
else if (!table.equals(other.table)) return false;
return true;
}
@Override
public String toString()
{
StringBuffer sb = new StringBuffer("SELECT"); //$NON-NLS-1$
if (distinct)
{
sb.append(" DISTINCT"); //$NON-NLS-1$
}
for (int i = 0; columns != null && i < columns.size(); i++)
{
sb.append(i == 0 ? ' ' : ',');
sb.append(columns.get(i).toString());
}
sb.append(" FROM ").append(table.toString()); //$NON-NLS-1$
if (conditions != null)
{
Iterator<Entry<String, AndCondition>> it = conditions.entrySet().iterator();
for (int i = 0; it.hasNext(); i++)
{
if (i > 0)
{
sb.append(" [AND] "); //$NON-NLS-1$
}
Map.Entry<String, AndCondition> entry = it.next();
sb.append(' ').append(entry.getKey().toString()).append(' ').append(entry.getValue().toString());
}
}
if (having != null)
{
sb.append(" HAVING ").append(having.toString()); //$NON-NLS-1$
}
for (int i = 0; joins != null && i < joins.size(); i++)
{
sb.append(' ').append(joins.get(i).toString());
}
if (sorts != null)
{
sb.append(" ORDER BY "); //$NON-NLS-1$
for (int i = 0; sorts != null && i < sorts.size(); i++)
{
if (i > 0)
{
sb.append(',');
}
sb.append(sorts.get(i).toString());
}
}
if (groupBy != null)
{
sb.append(" GROUP BY "); //$NON-NLS-1$
for (int i = 0; groupBy != null && i < groupBy.size(); i++)
{
if (i > 0)
{
sb.append(',');
}
sb.append(groupBy.get(i).toString());
}
}
if (lockMode != LOCK_MODE_NONE)
{
sb.append(" * LOCK "); //$NON-NLS-1$
sb.append(getLockModeString());
sb.append(" *"); //$NON-NLS-1$
}
return sb.toString();
}
///////// serialization ////////////////
@Override
public Object writeReplace()
{
// Note: when this serialized structure changes, make sure that old data (maybe saved as serialized xml) can still be deserialized!
return new ReplacedObject(QUERY_SERIALIZE_DOMAIN, getClass(), new Object[] { table, columns, new Byte((byte)(distinct ? 1 : 0 +
(plain_pk_select ? 2 : 0) + (4 * lockMode))), conditions, having, joins, sorts, groupBy });
}
@SuppressWarnings("unchecked")
public QuerySelect(ReplacedObject s)
{
Object[] members = (Object[])s.getObject();
int i = 0;
this.table = (QueryTable)members[i++];
this.columns = (ArrayList<IQuerySelectValue>)members[i++];
byte bits = ((Byte)members[i++]).byteValue();
this.distinct = (bits & 1) != 0;
this.plain_pk_select = (bits & 2) != 0;
this.lockMode = (bits / 4);
this.conditions = (HashMap<String, AndCondition>)members[i++];
Object hv = members[i++];
if (hv instanceof Map && ((Map)hv).size() > 0)
{
// old format Having was map, but only actually 1 key was used
this.having = (AndCondition)((Map)hv).values().iterator().next();
}
else
{
this.having = (AndCondition)hv;
}
this.joins = (ArrayList<ISQLJoin>)members[i++];
this.sorts = (ArrayList<IQuerySort>)members[i++];
this.groupBy = (ArrayList<IQuerySelectValue>)members[i++];
}
}