package edu.ualberta.med.biobank.server.reports;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.Type;
import edu.ualberta.med.biobank.common.reports.ReportsUtil;
import edu.ualberta.med.biobank.common.reports.filters.FilterOperator;
import edu.ualberta.med.biobank.common.reports.filters.FilterType;
import edu.ualberta.med.biobank.common.reports.filters.FilterTypes;
import edu.ualberta.med.biobank.model.EntityFilter;
import edu.ualberta.med.biobank.model.PropertyModifier;
import edu.ualberta.med.biobank.model.Report;
import edu.ualberta.med.biobank.model.ReportColumn;
import edu.ualberta.med.biobank.model.ReportFilter;
import edu.ualberta.med.biobank.model.ReportFilterValue;
import edu.ualberta.med.biobank.server.applicationservice.ReportData;
public class ReportRunner {
private static final String ID_COLUMN_NAME = "id"; //$NON-NLS-1$
private static final String PROPERTY_DELIMITER = "."; //$NON-NLS-1$
private static final String ALIAS_DELIMITER = "__"; //$NON-NLS-1$
private static final String PROPERTY_VALUE_TOKEN = "{value}"; //$NON-NLS-1$
private static final String MODIFIED_PROPERTY_ALIAS =
"_modifiedPropertyAlias"; //$NON-NLS-1$
private static final Comparator<ReportColumn> COMPARE_REPORT_COLUMN_POSITION =
new Comparator<ReportColumn>() {
@Override
public int compare(ReportColumn lhs, ReportColumn rhs) {
return lhs.getPosition() - rhs.getPosition();
}
};
private final Session session;
private final Report report;
private final Criteria criteria;
public ReportRunner(Session session, ReportData data) {
this.session = session;
this.report = data.getReport();
criteria = createCriteria();
if (criteria != null) {
criteria.setMaxResults(data.getMaxResults());
criteria.setFirstResult(data.getFirstRow());
if (data.getTimeout() > 0) {
criteria.setTimeout(data.getTimeout());
}
}
}
public List<?> run() {
if (criteria == null) {
return Arrays.asList();
}
return criteria.list();
}
private Collection<ReportColumn> getOrderedReportColumns() {
List<ReportColumn> orderedCols = new ArrayList<ReportColumn>();
loadProperty(report, "reportColumns"); //$NON-NLS-1$
Collection<ReportColumn> reportCols = report
.getReportColumns();
if (reportCols != null) {
orderedCols.addAll(reportCols);
}
Collections.sort(orderedCols, COMPARE_REPORT_COLUMN_POSITION);
return orderedCols;
}
private boolean isCount() {
Boolean isCount = report.getIsCount();
return isCount == null ? false : isCount;
}
private Criteria createCriteria() {
loadProperty(report, "reportColumns"); //$NON-NLS-1$
if (!isCount() && report.getReportColumns().isEmpty()) {
return null;
}
loadProperty(report, "entity"); //$NON-NLS-1$
Criteria criteria = session.createCriteria(report.getEntity()
.getClassName());
createAssociations(criteria);
ProjectionList pList = Projections.projectionList();
if (!isCount()) {
pList.add(Projections.property(ID_COLUMN_NAME));
} else {
// need to provide an alias for the column to be included in the
// results
pList.add(Projections.sqlProjection("NULL as null_value_", //$NON-NLS-1$
new String[] { "null_value_" }, //$NON-NLS-1$
new Type[] { StandardBasicTypes.INTEGER }));
}
int colNum = 1;
for (ReportColumn reportColumn : getOrderedReportColumns()) {
loadProperty(reportColumn, "entityColumn"); //$NON-NLS-1$
loadProperty(reportColumn.getEntityColumn(), "entityProperty"); //$NON-NLS-1$
String path = reportColumn.getEntityColumn().getEntityProperty()
.getProperty();
String aliasedProperty = getAliasedProperty(path);
Projection projection = null;
PropertyModifier propertyModifier = reportColumn
.getPropertyModifier();
if (propertyModifier != null) {
String sqlColumn = ReportsUtil.getSqlColumn(criteria,
aliasedProperty);
String modifier = propertyModifier.getPropertyModifier();
String modifiedProperty = modifier.replace(
PROPERTY_VALUE_TOKEN, sqlColumn);
String sqlAlias = MODIFIED_PROPERTY_ALIAS + colNum;
if (isCount()) {
projection = Projections.sqlGroupProjection(
modifiedProperty + " as " + sqlAlias, sqlAlias, //$NON-NLS-1$
new String[] { sqlAlias },
new Type[] { StandardBasicTypes.STRING });
} else {
projection = Projections.sqlProjection(modifiedProperty
+ " as " + sqlAlias, new String[] { sqlAlias }, //$NON-NLS-1$
new Type[] { StandardBasicTypes.STRING });
}
} else {
if (isCount()) {
projection = Projections.groupProperty(aliasedProperty);
} else {
projection = Projections.property(aliasedProperty);
}
}
pList.add(projection);
colNum++;
}
if (isCount()) {
pList.add(Projections.countDistinct(ID_COLUMN_NAME));
}
criteria.setProjection(pList);
loadProperty(report, "reportFilters"); //$NON-NLS-1$
Collection<ReportFilter> rfCollection = report
.getReportFilters();
if (rfCollection != null) {
for (ReportFilter reportFilter : rfCollection) {
loadProperty(reportFilter, "entityFilter"); //$NON-NLS-1$
loadProperty(reportFilter.getEntityFilter(), "entityProperty"); //$NON-NLS-1$
EntityFilter filter = reportFilter.getEntityFilter();
FilterType filterType = FilterTypes.getFilterType(filter
.getFilterType());
String propertyPath = filter.getEntityProperty().getProperty();
String aliasedProperty = getAliasedProperty(propertyPath);
Collection<ReportFilterValue> rfvCollection = reportFilter
.getReportFilterValues();
if (rfvCollection == null) {
rfvCollection = new HashSet<ReportFilterValue>();
}
FilterOperator op = null;
if (reportFilter.getOperator() != null) {
op = FilterOperator.getFilterOperator(reportFilter
.getOperator());
}
filterType.addCriteria(criteria, aliasedProperty, op,
new ArrayList<ReportFilterValue>(rfvCollection));
}
}
return criteria;
}
/**
* Read the property of the given <code>Object</code>. If the property
* exists and is an uninitialized Hibernate proxy object, then replace it
* with a copy from the database.
*
* @param object
* @param property
*/
private void loadProperty(Object object, String property) {
try {
Class<?> objectKlazz = object.getClass();
String methodSuffix = Character.toUpperCase(property.charAt(0))
+ property.substring(1);
Method getProperty = objectKlazz.getMethod("get" + methodSuffix); //$NON-NLS-1$
Class<?> propertyKlazz = getProperty.getReturnType();
Method setProperty = objectKlazz.getMethod("set" + methodSuffix, //$NON-NLS-1$
propertyKlazz);
Object propertyValue = getProperty.invoke(object);
if (Hibernate.isInitialized(propertyValue)) {
return;
}
// TODO: treat Collection-s differently
Class<?> proxyKlazz = propertyValue.getClass();
Method getId = proxyKlazz.getMethod("getId"); //$NON-NLS-1$
Serializable id = (Serializable) getId.invoke(propertyValue);
Object databaseObject = null;
if (id != null) {
databaseObject = session.load(propertyKlazz, id);
}
setProperty.invoke(object, databaseObject);
} catch (Exception e) {
// TODO: log this?
}
}
private void createAssociations(Criteria criteria) {
Set<String> createdPoperties = new HashSet<String>();
loadProperty(report, "reportColumns"); //$NON-NLS-1$
Collection<ReportColumn> cols = report.getReportColumns();
if (cols != null) {
for (ReportColumn reportColumn : cols) {
loadProperty(reportColumn, "entityColumn"); //$NON-NLS-1$
loadProperty(reportColumn.getEntityColumn(), "entityProperty"); //$NON-NLS-1$
String property = reportColumn.getEntityColumn()
.getEntityProperty().getProperty();
createAssociations(criteria, property, createdPoperties);
}
}
loadProperty(report, "reportFilters"); //$NON-NLS-1$
Collection<ReportFilter> filters = report.getReportFilters();
if (filters != null) {
for (ReportFilter filter : filters) {
loadProperty(filter, "entityFilter"); //$NON-NLS-1$
loadProperty(filter.getEntityFilter(), "entityProperty"); //$NON-NLS-1$
String property = filter.getEntityFilter().getEntityProperty()
.getProperty();
createAssociations(criteria, property, createdPoperties);
}
}
}
public static void createAssociations(Criteria criteria, String property) {
Set<String> createdProperties = getCreatedAssociations(criteria);
createAssociations(criteria, property, createdProperties);
}
private static Set<String> getCreatedAssociations(Criteria criteria) {
Set<String> createdProperties = new HashSet<String>();
@SuppressWarnings("rawtypes")
Iterator subcriterias = ((CriteriaImpl) criteria).iterateSubcriteria();
while (subcriterias.hasNext()) {
Criteria subcriteria = (Criteria) subcriterias.next();
String property = getProperty(subcriteria.getAlias());
if (property != null) {
createdProperties.add(property);
}
}
return createdProperties;
}
private static void createAssociations(Criteria criteria, String property,
Set<String> createdProperties) {
String parentProperty = getParentProperty(property);
while (parentProperty != null) {
if (!createdProperties.contains(parentProperty)) {
// Always use a left join to support the "is not set" option of
// many filters.
criteria.createCriteria(parentProperty,
getPropertyAlias(parentProperty), Criteria.LEFT_JOIN);
createdProperties.add(parentProperty);
}
parentProperty = getParentProperty(parentProperty);
}
}
private static String getPropertyAlias(String property) {
// TODO: do we need to swap delimiters? Can the alias name just include
// the period?
return property == null ? null : property.replace(PROPERTY_DELIMITER,
ALIAS_DELIMITER);
}
public static String getProperty(String parentProperty,
String... childProperties) {
StringBuilder sb = new StringBuilder();
if (parentProperty != null) {
sb.append(parentProperty);
sb.append(PROPERTY_DELIMITER);
}
sb.append(StringUtils.join(childProperties, PROPERTY_DELIMITER));
return sb.toString();
}
public static String getParentProperty(String property) {
String parentPath = null;
int lastDelimiter = property.lastIndexOf(PROPERTY_DELIMITER);
if (lastDelimiter != -1) {
parentPath = property.substring(0, lastDelimiter);
}
return parentPath;
}
public static String getProperty(String alias) {
return alias == null ? null : alias.replace(ALIAS_DELIMITER,
PROPERTY_DELIMITER);
}
public static String getAliasedProperty(String property) {
int lastDelimiter = property.lastIndexOf(PROPERTY_DELIMITER);
if (lastDelimiter != -1) {
String parentProperty = property.substring(0, lastDelimiter);
String lastProperty = property.substring(lastDelimiter + 1);
return getPropertyAlias(parentProperty) + PROPERTY_DELIMITER
+ lastProperty;
}
return property;
}
}