package javango.contrib.hibernate;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import javango.db.ManagerException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Value;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.Type;
public class HibernateQuerySetHelper {
private final static Log log = LogFactory.getLog(HibernateQuerySetHelper.class);
protected HibernateUtil hibernateUtil;
protected Class<?> modelClass;
protected DetachedCriteria criteria;
Map<String, DetachedCriteria> joins = new HashMap<String, DetachedCriteria>();
public HibernateQuerySetHelper(HibernateUtil hibernateUtil, Class modelClass, DetachedCriteria criteria) {
this.hibernateUtil = hibernateUtil;
this.modelClass = modelClass;
this.criteria = criteria;
}
/**
* Returns a the keys Class[]
* @return
* @throws DaoException
*/
public Class[] getPkClass() throws ManagerException {
Configuration cfg = hibernateUtil.getConfiguration();
PersistentClass pclass = cfg.getClassMapping(modelClass.getName());
if (pclass == null) {
throw new ManagerException("Unable to find class : "
+ modelClass.toString());
}
Property componentProperty = pclass.getIdentifierProperty();
if (componentProperty == null) {
Component component = pclass.getIdentifierMapper();
if (component == null) {
throw new ManagerException("Unable to get pk mapping");
}
if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));
Class<?>[] classArray = new Class[component.getColumnSpan()];
Iterator<Property> ite = component.getPropertyIterator();
int i = 0;
while (ite.hasNext()) {
Property p = ite.next();
if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
classArray[i++] = p.getType().getReturnedClass();
}
return classArray;
} else {
Value value = componentProperty.getValue();
if (value == null)
throw new ManagerException("Component value is null");
else {
if (log.isDebugEnabled()) log.debug(String.format("Found simple key for model %s '%s'",
modelClass.getName(), value.getType()
.getReturnedClass()));
return new Class[] { value.getType().getReturnedClass() };
}
}
}
public java.lang.reflect.Field findField(Class modelClass, String property) throws ManagerException {
// TODO dry, this is repeated in ModelForm, maybe helper/util class..
Class classToTry = modelClass;
while(classToTry != null) {
try {
return classToTry.getDeclaredField(property);
} catch (NoSuchFieldException e) {
classToTry = classToTry.getSuperclass();
}
}
throw new ManagerException("No such field found: " + property + " in class " + modelClass.getName());
}
public Object convert(String value, Class clazz) {
return ConvertUtils.convert(value, clazz);
}
public Object correctType(String fieldName, Object value) throws ManagerException {
return correctType(fieldName, value, this.modelClass);
}
public Object correctType(String fieldName, Object value, Class clazz) throws ManagerException {
// TODO May still need to convert non-strings to the correct value (ie long to double, etc).
if (!(value instanceof String)) {
return value;
}
ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(clazz);
if (metadata != null) {
Type type = metadata.getPropertyType(fieldName);
if (type instanceof CollectionType) {
log.warn("Unable to handle collection: " + fieldName);
return value;
} else if (type instanceof AssociationType) {
AssociationType assocType = (AssociationType)type;
Class[] assocPkClass = getPkClass();
if (assocPkClass.length > 1) {
log.error("Multi keyed associations are not supported in correctType");
return value;
}
Object o = convert((String)value, assocPkClass[0]);
return hibernateUtil.getSession().get(assocType.getReturnedClass(), (Serializable)o);
// TODO if return value is null should something be done??
//return value;
} else {
return convert((String)value, type.getReturnedClass());
}
}
// TODO is this code needed with the type.getReturnedClass from above??
java.lang.reflect.Field field = findField(clazz, fieldName);
return convert((String)value, field.getType());
}
public Object convertToPk(Object pk) throws ManagerException {
if (pk == null) return null;
Class[] keyClass = getPkClass();
if (keyClass.length != 1) {
throw new ManagerException("get(pk) not supported for composite keyed models");
}
if (pk.getClass().equals(keyClass[0])) {
return pk;
}
if (log.isDebugEnabled()) log.debug("Trying to convert input to type " + keyClass[0]);
return convert(pk.toString(), keyClass[0]);
}
public void resolveProperty() {
}
protected Criterion getCriterion(Class searchClass, String searchType, String searchField, Object searchValue, boolean property) throws ManagerException {
if ("like".equals(searchType)) {
// the as400 in particular does not like likes without percents on char fields
if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
return Restrictions.like(searchField, searchValue);
}
if (property) {
return Restrictions.eqProperty(searchField, (String)searchValue);
} else {
return Restrictions.eq(searchField, searchValue);
}
} else if ("ilike".equals(searchType)) {
if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
return Restrictions.ilike(searchField, searchValue);
}
return Restrictions.eq(searchField, searchValue).ignoreCase();
} else if ("nlike".equals(searchType)) {
// the as400 in particular does not like likes without percents on char fields
if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
return Restrictions.not(Restrictions.like(searchField, searchValue));
}
return Restrictions.ne(searchField, searchValue);
} else if ("nilike".equals(searchType)) {
if (searchValue instanceof String && StringUtils.contains((String)searchValue, "%")) {
return Restrictions.not(Restrictions.ilike(searchField, searchValue));
}
return Restrictions.ne(searchField, searchValue).ignoreCase();
} else if ("lte".equals(searchType)) {
if (property) {
return Restrictions.leProperty(searchField, (String)searchValue);
} else {
return Restrictions.le(searchField, searchValue);
}
} else if ("gte".equals(searchType)) {
if (property) {
return Restrictions.geProperty(searchField, (String)searchValue);
} else {
return Restrictions.ge(searchField, searchValue);
}
} else if ("lt".equals(searchType)) {
if (property) {
return Restrictions.ltProperty(searchField, (String)searchValue);
} else {
return Restrictions.lt(searchField, searchValue);
}
} else if ("gt".equals(searchType)) {
if (property){
return Restrictions.gtProperty(searchField, (String)searchValue);
} else {
return Restrictions.gt(searchField, searchValue);
}
} else if ("ilte".equals(searchType)) {
if (property) {
return Restrictions.leProperty(searchField, (String)searchValue);
} else {
return Restrictions.le(searchField, searchValue).ignoreCase();
}
} else if ("igte".equals(searchType)) {
if (property) {
return Restrictions.geProperty(searchField, (String)searchValue);
} else {
return Restrictions.ge(searchField, searchValue).ignoreCase();
}
} else if ("ilt".equals(searchType)) {
if (property) {
return Restrictions.ltProperty(searchField, (String)searchValue);
} else {
return Restrictions.lt(searchField, searchValue).ignoreCase();
}
} else if ("igt".equals(searchType)) {
if (property){
return Restrictions.gtProperty(searchField, (String)searchValue);
} else {
return Restrictions.gt(searchField, searchValue).ignoreCase();
}
} else if ("eq".equals(searchType)) {
if (searchValue == null) {
return Restrictions.isNull(searchField);
}
if (property) {
return Restrictions.eqProperty(searchField, (String)searchValue);
} else {
return Restrictions.eq(searchField, searchValue);
}
} else if ("ne".equals(searchType)) {
if (searchValue == null) {
return Restrictions.isNotNull(searchField);
}
if (property) {
return Restrictions.neProperty(searchField, (String)searchValue);
} else {
return Restrictions.ne(searchField, searchValue);
}
} else if ("in".equals(searchType)) {
if (searchValue instanceof DetachedCriteria) {
return org.hibernate.criterion.Property.forName(searchField).in((DetachedCriteria)searchValue);
} else if (searchValue instanceof HibernateQuerySet<?>) {
DetachedCriteria newC = ((HibernateQuerySet<?>)searchValue).getRawCriteria();
newC.setProjection(Projections.id());
return org.hibernate.criterion.Property.forName(searchField).in(newC);
} else if (searchValue instanceof Collection<?>) {
Collection<?> collection = (Collection<?>)searchValue;
return Restrictions.in(searchField, collection);
} else {
return Restrictions.in(searchField, (Object[])searchValue);
}
} else if ("notin".equals(searchType)) {
return Restrictions.not(Restrictions.in(searchField, (Object[])searchValue));
} else if ("date".equals(searchType)) {
if (!(searchValue instanceof Date) || searchValue == null) {
throw new ManagerException ("__date search requires a non-null instance of java.util.Date");
}
Date date = (Date)searchValue;
Calendar from = new GregorianCalendar();
from.setTime(date);
from.set(Calendar.HOUR_OF_DAY, 0);
from.set(Calendar.MINUTE, 0);
from.set(Calendar.SECOND, 0);
from.set(Calendar.MILLISECOND, 0);
Calendar to = new GregorianCalendar();
to.setTime(date);
to.set(Calendar.HOUR_OF_DAY, 23);
to.set(Calendar.MINUTE, 59);
to.set(Calendar.SECOND, 59);
to.set(Calendar.MILLISECOND, 0);
return Restrictions.between(searchField, from.getTime(), to.getTime());
}
return null;
}
protected Object[] resolveJoins(String[] parts, int count) throws ManagerException {
DetachedCriteria c = criteria; // start with the main criteria, if we have joins this will eventually be the criteria for the join we want.
int part = 0; // the part we are looking at. process util we get through them all.
Class searchClass = this.modelClass;
String joinPath = null; // this gets built to the full path for the joins map's key
if (count > 0) {
// we got some joins.. resolve them
while (part < count) { // get all but the last two which may be a searchProperty and searchType
String joinProperty = parts[part];
joinPath = joinPath == null ? joinProperty : joinPath + "__" + joinProperty;
DetachedCriteria subC = joins.get(joinPath);
if (subC == null) {
subC = c.createCriteria(joinProperty, joinPath, Criteria.LEFT_JOIN);
joins.put(joinPath, subC);
}
ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(searchClass);
Type type = metadata.getPropertyType(joinProperty);
if (type instanceof CollectionType) {
log.info("Expiremental CollectionType join: " + joinPath);
subC.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
Type newtype = ((CollectionType)type).getElementType((SessionFactoryImplementor)hibernateUtil.getSessionFactory());
searchClass = newtype.getReturnedClass();
} else {
searchClass = type.getReturnedClass();
}
c = subC;
part++;
}
}
return new Object[]{searchClass, c};
}
public void processField(String fieldName, Object value, boolean property) throws ManagerException {
String[] parts = fieldName.split("__");
if (parts.length == 1) { // no __ found in the string, default to simple equals match
Object[] o = resolveJoins(parts, 0);
Class searchClass = (Class)o[0];
Object searchValue = property ? value : correctType(fieldName, value, searchClass);
criteria.add(getCriterion(searchClass, "eq", fieldName, searchValue, property));
return;
}
String searchType = parts[parts.length-1];
String searchField = parts[parts.length-2];
Object[] o = resolveJoins(parts, parts.length-2);
Class searchClass = (Class)o[0];
DetachedCriteria c = (DetachedCriteria)o[1];
Object searchValue = property ? value : correctType(searchField, value, searchClass);
Criterion criterion = getCriterion(searchClass, searchType, searchField, searchValue, property);
if (criterion == null) { // humm no match on the search type.. assume equals
searchField = searchType;
// searchType = "eq"; // not really needed
o = resolveJoins(parts, parts.length-1);
searchClass = (Class)o[0];
c = (DetachedCriteria)o[1];
if (property) {
criterion = Restrictions.eqProperty(searchField, (String)searchValue);
} else {
criterion = Restrictions.eq(searchField, correctType(searchField, value, searchClass));
}
}
c.add(criterion);
}
/**
* Update the property values from the map, to preserve backwards compat null values in the map are ignored.
*
* @param params
* @throws ManagerException
*/
public void updateCriteriaFromMap(Map<String, Object> params) throws ManagerException {
for(Entry<String, Object> entry : params.entrySet()) {
processField(entry.getKey(), entry.getValue(), false);
}
}
public void updateCriteriaFromPropertyMap(Map<String, String> params) throws ManagerException {
if (params == null) return;
for(Entry<String, String> entry : params.entrySet()) {
processField(entry.getKey(), entry.getValue(), true);
}
}
@SuppressWarnings("unchecked")
public void updateCriteria(Object params) throws ManagerException {
if (params == null) return;
if (params instanceof Map) {
updateCriteriaFromMap((Map)params);
return;
}
try {
Class c = params.getClass();
java.lang.reflect.Field[] fields = c.getDeclaredFields();
for (int i=0; i<fields.length; i++) {
String fieldName = fields[i].getName();
Object value = PropertyUtils.getProperty(params, fieldName);
processField(fieldName, value, false);
}
} catch (IllegalAccessException e) {
log.error(e,e);
} catch (InvocationTargetException e) {
log.error(e,e);
} catch (NoSuchMethodException e) {
log.error(e,e);
}
}
/**
* Returns a the keys Class[]
* @return
* @throws DaoException
*/
public String getPkProperty() throws ManagerException {
Configuration cfg = hibernateUtil.getConfiguration();
PersistentClass pclass = cfg.getClassMapping(modelClass.getName());
if (pclass == null) {
throw new ManagerException("Unable to find class : "
+ modelClass.toString());
}
Property componentProperty = pclass.getIdentifierProperty();
if (componentProperty == null) {
throw new UnsupportedOperationException("Multiple primary keys not supported");
// Component component = pclass.getIdentifierMapper();
// if (component == null) {
// throw new DaoException("Unable to get pk mapping");
// }
// if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));
//
// Class<?>[] classArray = new Class[component.getColumnSpan()];
// Iterator<Property> ite = component.getPropertyIterator();
// int i = 0;
// while (ite.hasNext()) {
// Property p = ite.next();
// if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
// classArray[i++] = p.getType().getReturnedClass();
// }
// return classArray;
} else {
return componentProperty.getName();
}
}
public void addOrderBy(String orderBy) throws ManagerException {
boolean desc = orderBy.startsWith("-");
if (desc) {
orderBy = orderBy.substring(1);
}
DetachedCriteria c = criteria;
String[] parts = orderBy.split("__");
if (parts.length > 1) {
Object[] o = resolveJoins(parts, parts.length-1);
// Class searchClass = (Class)o[0];
c = (DetachedCriteria)o[1];
orderBy = parts[parts.length-1];
}
if (desc) {
c.addOrder(Order.desc(orderBy));
} else {
c.addOrder(Order.asc(orderBy));
}
}
}