cloudstack/utils/src/com/cloud/utils/db/GenericSearchBuilder.java
2010-09-17 13:37:53 -07:00

408 lines
15 KiB
Java

/**
* Copyright (C) 2010 Cloud.com, Inc. All rights reserved.
*
* This software is licensed under the GNU General Public License v3 or later.
*
* It is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.cloud.utils.db;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import com.cloud.utils.Ternary;
import com.cloud.utils.db.SearchCriteria.Func;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.SearchCriteria.SelectType;
import com.cloud.utils.exception.CloudRuntimeException;
/**
* GenericSearchBuilder is used to build a search based on a VO object
* and return it in an object specified by the caller. Note that the
* VO object and the return object can be the same type. There is
* a convenience class provided called SearchBuilder that provides
* exactly that functionality.
*
* @param <T> VO object this Search is build for.
* @param <K> Result object that should contain the results.
*/
public class GenericSearchBuilder<T, K> implements MethodInterceptor {
final protected Map<String, Attribute> _attrs;
protected ArrayList<Condition> _conditions;
protected HashMap<String, Ternary<GenericSearchBuilder<?, ?>, Attribute, Attribute>> _joins;
protected ArrayList<Select> _selects;
protected ArrayList<Attribute> _groupBys;
protected Class<T> _entityBeanType;
protected Class<K> _resultType;
protected SelectType _selectType;
protected T _entity;
protected ArrayList<Attribute> _specifiedAttrs;
@SuppressWarnings("unchecked")
protected GenericSearchBuilder(T entity, Class<K> clazz, Map<String, Attribute> attrs) {
_entityBeanType = (Class<T>)entity.getClass();
_resultType = clazz;
_attrs = attrs;
_entity = entity;
_conditions = new ArrayList<Condition>();
_joins = null;
_specifiedAttrs = new ArrayList<Attribute>();
}
public T entity() {
return _entity;
}
public GenericSearchBuilder<T, K> selectField(Object... useless) {
assert _entity != null : "SearchBuilder cannot be modified once it has been setup";
assert _specifiedAttrs.size() > 0 : "You didn't specify any attributes";
if (_selects == null) {
_selects = new ArrayList<Select>();
}
for (Attribute attr : _specifiedAttrs) {
Field field = null;
try {
field = _resultType.getDeclaredField(attr.field.getName());
field.setAccessible(true);
} catch (SecurityException e) {
} catch (NoSuchFieldException e) {
}
_selects.add(new Select(Func.NATIVE, attr, field, null));
}
_specifiedAttrs.clear();
return this;
}
public GenericSearchBuilder<T, K> select(String fieldName, Func func, Object useless, Object... params) {
assert _entity != null : "SearchBuilder cannot be modified once it has been setup";
assert _specifiedAttrs.size() <= 1 : "You can't specify more than one field to search on";
assert func.getCount() == -1 || (func.getCount() == (params.length + 1)) : "The number of parameters does not match the function param count for " + func;
if (_selects == null) {
_selects = new ArrayList<Select>();
}
Field field = null;
if (fieldName != null) {
try {
field = _resultType.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (SecurityException e) {
throw new CloudRuntimeException("Unable to find " + fieldName, e);
} catch (NoSuchFieldException e) {
throw new CloudRuntimeException("Unable to find " + fieldName, e);
}
} else {
assert _selects.size() == 0 : "You're selecting more than one item and yet is not providing a container class to put these items in. So what do you expect me to do. Spin magic?";
}
Select select = new Select(func, _specifiedAttrs.size() == 0 ? null : _specifiedAttrs.get(0), field, params);
_selects.add(select);
_specifiedAttrs.clear();
return this;
}
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
String name = method.getName();
if (name.startsWith("get")) {
String fieldName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
set(fieldName);
} else if (name.startsWith("is")) {
String fieldName = Character.toLowerCase(name.charAt(2)) + name.substring(3);
set(fieldName);
} else {
assert false : "Perhaps you need to make the method start with get or is?";
}
return methodProxy.invokeSuper(object, args);
}
protected void set(String name) {
Attribute attr = _attrs.get(name);
assert (attr != null) : "Searching for a field that's not there: " + name;
_specifiedAttrs.add(attr);
}
/**
* Adds an AND condition to the SearchBuilder.
*
* @param name param name you will use later to set the values in this search condition.
* @param useless SearchBuilder.entity().get*() which refers to the field that you're searching on.
* @param op operation to apply to the field.
* @return this
*/
public GenericSearchBuilder<T, K> and(String name, Object useless, Op op) {
constructCondition(name, " AND ", _specifiedAttrs.get(0), op);
return this;
}
public GenericSearchBuilder<T, K> and() {
constructCondition(null, " AND ", null, null);
return this;
}
public GenericSearchBuilder<T, K> where() {
return and();
}
public GenericSearchBuilder<T, K> or() {
constructCondition(null, " OR ", null, null);
return this;
}
public GenericSearchBuilder<T, K> where(String name, Object useless, Op op) {
return and(name, useless, op);
}
public GenericSearchBuilder<T, K> left(String name, Object useless, Op op) {
constructCondition(name, " ( ", _specifiedAttrs.get(0), op);
return this;
}
public GenericSearchBuilder<T, K> op(String name, Object useless, Op op) {
return left(name, useless, op);
}
public GenericSearchBuilder<T, K> openParen(String name, Object useless, Op op) {
return left(name, useless, op);
}
public GenericSearchBuilder<T, K> groupBy(Object... useless) {
if(_groupBys == null) {
_groupBys = new ArrayList<Attribute>();
}
Attribute[] attrs = _specifiedAttrs.toArray(new Attribute[_specifiedAttrs.size()]);
for(Attribute attr : attrs)
_groupBys.add(attr);
_specifiedAttrs.clear();
return this;
}
/**
* Adds an OR condition to the SearchBuilder.
*
* @param name param name you will use later to set the values in this search condition.
* @param useless SearchBuilder.entity().get*() which refers to the field that you're searching on.
* @param op operation to apply to the field.
* @return this
*/
public GenericSearchBuilder<T, K> or(String name, Object useless, Op op) {
constructCondition(name, " OR ", _specifiedAttrs.get(0), op);
return this;
}
public GenericSearchBuilder<T, K> join(String name, GenericSearchBuilder<?, ?> builder, Object useless, Object useless2) {
assert _entity != null : "SearchBuilder cannot be modified once it has been setup";
assert _specifiedAttrs.size() == 1 : "You didn't select the attribute.";
assert builder._entity != null : "SearchBuilder cannot be modified once it has been setup";
assert builder._specifiedAttrs.size() == 1 : "You didn't select the attribute.";
assert builder != this : "You can't add yourself, can you? Really think about it!";
Ternary<GenericSearchBuilder<?, ?>, Attribute, Attribute> t = new Ternary<GenericSearchBuilder<?, ?>, Attribute, Attribute>(builder, _specifiedAttrs.get(0), builder._specifiedAttrs.get(0));
if (_joins == null) {
_joins = new HashMap<String, Ternary<GenericSearchBuilder<?, ?>, Attribute, Attribute>>();
}
_joins.put(name, t);
builder._specifiedAttrs.clear();
_specifiedAttrs.clear();
return this;
}
protected void constructCondition(String conditionName, String cond, Attribute attr, Op op) {
assert _entity != null : "SearchBuilder cannot be modified once it has been setup";
assert op == null || _specifiedAttrs.size() == 1 : "You didn't select the attribute.";
assert op != Op.SC : "Call join";
Condition condition = new Condition(conditionName, cond, attr, op);
_conditions.add(condition);
_specifiedAttrs.clear();
}
/**
* creates the SearchCriteria so the actual values can be filled in.
*
* @return SearchCriteria
*/
public SearchCriteria<K> create() {
if (_entity != null) {
done();
}
return new SearchCriteria<K>(this);
}
public SearchCriteria<K> create(String name, Object... values) {
SearchCriteria<K> sc = create();
sc.setParameters(name, values);
return sc;
}
public GenericSearchBuilder<T, K> right() {
Condition condition = new Condition("rp", " ) ", null, Op.RP);
_conditions.add(condition);
return this;
}
public GenericSearchBuilder<T, K> cp() {
return right();
}
public GenericSearchBuilder<T, K> closeParen() {
return right();
}
public SelectType getSelectType() {
return _selectType;
}
/**
* Marks the SearchBuilder as completed in building the search conditions.
*/
public synchronized void done() {
if (_entity != null) {
Factory factory = (Factory)_entity;
factory.setCallback(0, null);
_entity = null;
}
if (_joins != null) {
for (Ternary<GenericSearchBuilder<?, ?>, Attribute, Attribute> join : _joins.values()) {
join.first().done();
}
}
if (_selects == null || _selects.size() == 0) {
_selectType = SelectType.Entity;
assert _entityBeanType.equals(_resultType) : "Expecting " + _entityBeanType + " because you didn't specify any selects but instead got " + _resultType;
return;
}
for (Select select : _selects) {
if (select.field == null) {
assert (_selects.size() == 1) : "You didn't specify any fields to put the result in but you're specifying more than one select so where should I put the selects?";
_selectType = SelectType.Single;
return;
}
if (select.func != null) {
_selectType = SelectType.Result;
return;
}
}
_selectType = SelectType.Fields;
}
protected static class Condition {
protected final String name;
protected final String cond;
protected final Op op;
protected final Attribute attr;
protected Condition(String name) {
this(name, null, null, null);
}
public Condition(String name, String cond, Attribute attr, Op op) {
this.name = name;
this.attr = attr;
this.cond = cond;
this.op = op;
}
public void toSql(StringBuilder sql, Object[] params, int count) {
if (count > 0) {
sql.append(cond);
}
if (op == null) {
return;
}
if (op == Op.SC) {
sql.append(" (").append(((SearchCriteria)params[0]).getWhereClause()).append(") ");
return;
}
if (attr == null) {
return;
}
sql.append(attr.table).append(".").append(attr.columnName).append(op.toString());
if (op.getParams() == -1) {
for (int i = 0; i < params.length; i++) {
sql.insert(sql.length() - 2, "?,");
}
sql.delete(sql.length() - 3, sql.length() - 2); // remove the last ,
} else if (op == Op.EQ && (params == null || params.length == 0 || params[0] == null)) {
sql.delete(sql.length() - 4, sql.length());
sql.append(" IS NULL ");
} else if (op == Op.NEQ && (params == null || params.length == 0 || params[0] == null)) {
sql.delete(sql.length() - 5, sql.length());
sql.append(" IS NOT NULL ");
} else {
assert((op.getParams() == 0 && params == null) || (params.length == op.getParams())) : "Problem with condition: " + name;
}
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Condition)) {
return false;
}
Condition condition = (Condition)obj;
return name.equals(condition.name);
}
}
protected static class Select {
public Func func;
public Attribute attr;
public Object[] params;
public Field field;
protected Select() {
}
public Select(Func func, Attribute attr, Field field, Object[] params) {
this.func = func;
this.attr = attr;
this.params = params;
this.field = field;
}
}
}