/*
* gvNIX is an open source tool for rapid application development (RAD).
* Copyright (C) 2010 Generalitat Valenciana
*
* This program 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 (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 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 org.gvnix.web.datatables.util;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.gvnix.web.datatables.query.SearchResults;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import com.github.dandelion.datatables.core.ajax.ColumnDef;
import com.github.dandelion.datatables.core.ajax.ColumnDef.SortDirection;
import com.github.dandelion.datatables.core.ajax.DataSet;
import com.github.dandelion.datatables.core.ajax.DatatablesCriterias;
import com.github.dandelion.datatables.core.export.ExportConf;
import com.github.dandelion.datatables.core.export.HtmlTableBuilder;
import com.github.dandelion.datatables.core.export.HtmlTableBuilder.BeforeEndStep;
import com.github.dandelion.datatables.core.export.HtmlTableBuilder.ColumnStep;
import com.github.dandelion.datatables.core.html.HtmlTable;
import com.mysema.query.BooleanBuilder;
import com.mysema.query.QueryModifiers;
import com.mysema.query.jpa.impl.JPAQuery;
import com.mysema.query.types.Order;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Predicate;
import com.mysema.query.types.expr.BooleanExpression;
import com.mysema.query.types.path.PathBuilder;
/**
* Datatables utility functions
*
* @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a
* href="http://www.dgti.gva.es">General Directorate for Information
* Technologies (DGTI)</a>
*
* @deprecated use {@link DatatablesUtilsBean} instead
*/
@Deprecated
public class DatatablesUtils {
private static final String ISNULL_OPE = "ISNULL";
private static final String NOTNULL_OPE = "NOTNULL";
private static final String G_ISNULL_OPE = "global.filters.operations.all.isnull";
private static final String G_NOTNULL_OPE = "global.filters.operations.all.notnull";
// Logger
private static Logger LOGGER = LoggerFactory
.getLogger(DatatablesUtils.class);
public static final String ROWS_ON_TOP_IDS_PARAM = "dtt_row_on_top_ids";
private static final String SEPARATOR_FIELDS = ".";
private static final String SEPARATOR_FIELDS_ESCAPED = "_~~_";
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
ConversionService conversionService, MessageSource messageSource) {
return findByCriteria(entityClass, null, null, entityManager,
datatablesCriterias, (BooleanBuilder) null, false,
conversionService, messageSource, null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
EntityManager entityManager, DatatablesCriterias datatablesCriterias) {
return findByCriteria(entityClass, null, null, entityManager,
datatablesCriterias, (BooleanBuilder) null, false, null, null,
null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap,
ConversionService conversionService, MessageSource messageSource) {
return findByCriteria(entityClass, null, null, entityManager,
datatablesCriterias, baseSearchValuesMap, false,
conversionService, messageSource);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap) {
return findByCriteria(entityClass, null, null, entityManager,
datatablesCriterias, baseSearchValuesMap, false, null, null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
ConversionService conversionService, MessageSource messageSource) {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias, null,
false, conversionService, messageSource, null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager, DatatablesCriterias datatablesCriterias) {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias, null,
false, null, null, null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
* <p/>
* This method can receive rows-on-top as parameter on
* <code>baseSearchValueMap</code> using {@link #ROWS_ON_TOP_IDS_PARAM}
* name.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap,
ConversionService conversionService, MessageSource messageSource) {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
baseSearchValuesMap, false, conversionService, messageSource);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @return
* @deprecated see
* {@link #findByCriteria(Class, EntityManager, DatatablesCriterias, Map, ConversionService, MessageSource)}
*/
public static <T> SearchResults<T> findByCriteria(Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap) {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
baseSearchValuesMap, false, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @param distinct use distinct query
* @return
* @deprecated see
* {@link #findByCriteria(Class, EntityManager, DatatablesCriterias, Map,boolean, ConversionService, MessageSource)}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap, boolean distinct)
throws IllegalArgumentException {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
baseSearchValuesMap, distinct, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
* <p/>
* This method can receive rows-on-top as parameter on
* <code>baseSearchValueMap</code> using {@link #ROWS_ON_TOP_IDS_PARAM}
* name.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param baseSearchValuesMap (optional) base filter values
* @param distinct use distinct query
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
Map<String, Object> baseSearchValuesMap, boolean distinct,
ConversionService conversionService, MessageSource messageSource)
throws IllegalArgumentException {
Assert.notNull(entityClass);
// Query DSL builder
PathBuilder<T> entity = new PathBuilder<T>(entityClass, "entity");
Object[] rowsOnTopIds = null;
// Predicate for base query
BooleanBuilder basePredicate;
if (baseSearchValuesMap != null) {
LOGGER.debug(
"findByCriteria handle baseSearch by map-of-values for entity '{}'...",
entity.getType());
// Handle ROWS_ON_TOP_IDS_PARAM param
Object tmpObject = baseSearchValuesMap.get(ROWS_ON_TOP_IDS_PARAM);
if (tmpObject != null) {
// Check if value is an array, otherwise
if (tmpObject.getClass().isArray()) {
rowsOnTopIds = (Object[]) tmpObject;
}
else {
rowsOnTopIds = new Object[] { tmpObject };
}
Map<String, Object> newBaseSearch = new HashMap<String, Object>(
baseSearchValuesMap);
newBaseSearch.remove(ROWS_ON_TOP_IDS_PARAM);
LOGGER.trace("findByCriteria extract rows on top from map {}",
rowsOnTopIds);
basePredicate = QuerydslUtils.createPredicateByAnd(entity,
newBaseSearch, conversionService);
}
else {
basePredicate = QuerydslUtils.createPredicateByAnd(entity,
baseSearchValuesMap, conversionService);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findByCriteria baseSearch by map-of-values: {}",
basePredicate.toString());
}
}
else {
basePredicate = new BooleanBuilder();
}
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, distinct, conversionService, messageSource,
rowsOnTopIds);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param distinct use distinct query
* @return
* @deprecated {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, boolean distinct)
throws IllegalArgumentException {
return findByCriteria(entityClass, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, distinct, null, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param distinct use distinct query
* @return
* @deprecated see
* {@link #findByCriteria(Class, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, boolean distinct,
ConversionService conversionService, MessageSource messageSource)
throws IllegalArgumentException {
Assert.notNull(entityClass);
// Query DSL builder
PathBuilder<T> entity = new PathBuilder<T>(entityClass, "entity");
return findByCriteria(entity, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, distinct, conversionService, messageSource, null);
}
/**
* Execute a select query on entityClass using {@code DatatablesCriterias}
* information for filter, sort and paginate result.
* <p/>
* This method can receive rows-on-top as parameter on
* <code>baseSearchValueMap</code> using {@link #ROWS_ON_TOP_IDS_PARAM}
* name.
*
* @param entityClass entity to use in search
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @param rowsOnTopIds (optional) array with id of rows to show on top of
* result list
* @return
* @throws IllegalArgumentException
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
Class<T> entityClass,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, boolean distinct,
ConversionService conversionService, MessageSource messageSource,
Object[] rowsOnTopIds) throws IllegalArgumentException {
Assert.notNull(entityClass);
// Query DSL builder
PathBuilder<T> entity = new PathBuilder<T>(entityClass, "entity");
return findByCriteria(entity, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, distinct, conversionService, messageSource,
rowsOnTopIds);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param distinct use distinct query
* @return
* @deprecated see
* {@link #findByCriteria(Class, EntityManager, DatatablesCriterias, Map, ConversionService, MessageSource)}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, ConversionService conversionService,
MessageSource messageSource) throws IllegalArgumentException {
return findByCriteria(entity, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, false, conversionService, messageSource, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @return
* @deprecated see
* {@link #findByCriteria(PathBuilder, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate) throws IllegalArgumentException {
return findByCriteria(entity, filterByAssociations,
orderByAssociations, entityManager, datatablesCriterias,
basePredicate, false, null, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
* @deprecated see
* {@link #findByCriteria(PathBuilder, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity, EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, ConversionService conversionService,
MessageSource messageSource) throws IllegalArgumentException {
return findByCriteria(entity, null, null, entityManager,
datatablesCriterias, basePredicate, false, conversionService,
messageSource, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @param rowsOnTopIds (optional) array with id of rows to show on top of
* result list
* @return
* @throws IllegalArgumentException
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity, EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, ConversionService conversionService,
MessageSource messageSource, Object[] rowsOnTopIds)
throws IllegalArgumentException {
return findByCriteria(entity, null, null, entityManager,
datatablesCriterias, basePredicate, false, conversionService,
messageSource, rowsOnTopIds);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @return
* @deprecated see
* {@link #findByCriteria(PathBuilder, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity, EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate) throws IllegalArgumentException {
return findByCriteria(entity, null, null, entityManager,
datatablesCriterias, basePredicate, false, null, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param distinct use distinct query
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @return
* @deprecated see
* {@link #findByCriteria(PathBuilder, Map, Map, EntityManager, DatatablesCriterias, BooleanBuilder, boolean, ConversionService, MessageSource, Object[])}
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, boolean distinct,
ConversionService conversionService, MessageSource messageSource)
throws IllegalArgumentException {
return findByCriteria(entity, null, null, entityManager,
datatablesCriterias, basePredicate, false, null, null, null);
}
/**
* Execute a select query on entityClass using <a
* href="http://www.querydsl.com/">Querydsl</a> which enables the
* construction of type-safe SQL-like queries.
*
* @param entity builder for entity to use in search. Represents the entity
* and gives access to its properties for query purposes
* @param filterByAssociations (optional) for each related entity to join
* contain as key the name of the association and as value the List
* of related entity fields to filter by
* @param orderByAssociations (optional) for each related entity to order
* contain as key the name of the association and as value the List
* of related entity fields to order by
* @param entityManager {@code entityClass} {@link EntityManager}
* @param datatablesCriterias datatables parameters for query
* @param basePredicate (optional) base filter conditions
* @param distinct use distinct query
* @param conversionService required by filter-by-expression and rows-on-top
* (otherwise optional)
* @param messageSource required by filter-by-expression (otherwise
* optional)
* @param rowsOnTopIds (optional) array with id of rows to show on top of
* result list
* @return
* @throws IllegalArgumentException
*/
public static <T, E extends Comparable<?>> SearchResults<T> findByCriteria(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
Map<String, List<String>> orderByAssociations,
EntityManager entityManager,
DatatablesCriterias datatablesCriterias,
BooleanBuilder basePredicate, boolean distinct,
ConversionService conversionService, MessageSource messageSource,
Object[] rowsOnTopIds) throws IllegalArgumentException {
// Check arguments aren't null
Assert.notNull(entityManager);
Assert.notNull(datatablesCriterias);
// If null, create empty Map to avoid control code overload
if (CollectionUtils.isEmpty(filterByAssociations)) {
filterByAssociations = new HashMap<String, List<String>>();
}
if (CollectionUtils.isEmpty(orderByAssociations)) {
orderByAssociations = new HashMap<String, List<String>>();
}
// true if data results must be paginated
boolean isPaged = datatablesCriterias.getDisplaySize() != null
&& datatablesCriterias.getDisplaySize() > 0;
// true if the search must take in account all columns
boolean findInAllColumns = StringUtils.isNotEmpty(datatablesCriterias
.getSearch()) && datatablesCriterias.hasOneFilterableColumn();
LOGGER.debug(
"findByCriteria for entity '{}' (paged={} findInAllColumns={})",
entity.getType(), isPaged, findInAllColumns);
// ----- Create queries -----
// query will take in account datatables search, order and paging
// criterias
JPAQuery query = new JPAQuery(entityManager);
query = query.from(entity);
// baseQuery will use base search values only in order to count
// all for success paging
JPAQuery baseQuery = new JPAQuery(entityManager);
baseQuery = baseQuery.from(entity);
// ----- Entity associations for Query JOINs, ORDER BY, ... -----
Map<String, PathBuilder<?>> associationMap = new HashMap<String, PathBuilder<?>>();
query = prepareQueryAssociationMap(entity, filterByAssociations,
datatablesCriterias, findInAllColumns, query, associationMap);
// ----- Query WHERE clauses -----
// Filters by column. Using BooleanBuilder, a cascading builder for
// Predicate expressions
BooleanBuilder filtersByColumnPredicate = new BooleanBuilder();
// Filters by table (for all columns)
BooleanBuilder filtersByTablePredicate = new BooleanBuilder();
try {
// Build the filters by column expression
if (datatablesCriterias.hasOneFilteredColumn()) {
filtersByColumnPredicate = prepareQueryFilterPart(entity,
filterByAssociations, datatablesCriterias,
associationMap, filtersByColumnPredicate,
conversionService, messageSource);
}
// Build the query to search the given value in all columns
filtersByTablePredicate = prepareQuerySearchPart(entity,
filterByAssociations, datatablesCriterias,
findInAllColumns, associationMap, filtersByTablePredicate,
conversionService);
}
catch (Exception e) {
LOGGER.error("Exception preparing filter for entity {}",
entity.getType(), e);
SearchResults<T> searchResults = new SearchResults<T>(
new ArrayList<T>(0), 0, isPaged, new Long(
org.apache.commons.lang3.ObjectUtils.defaultIfNull(
datatablesCriterias.getDisplayStart(), 0)),
new Long(org.apache.commons.lang3.ObjectUtils
.defaultIfNull(
datatablesCriterias.getDisplaySize(), 0)),
0);
return searchResults;
}
// ----- Query ORDER BY -----
List<OrderSpecifier<?>> orderSpecifiersList = prepareQueryOrder(entity,
orderByAssociations, datatablesCriterias, associationMap);
// ----- Query results paging -----
Long offset = null;
Long limit = null;
if (isPaged) {
limit = new Long(datatablesCriterias.getDisplaySize());
}
if (datatablesCriterias.getDisplayStart() != null
&& datatablesCriterias.getDisplayStart() >= 0) {
offset = new Long(datatablesCriterias.getDisplayStart());
}
// ------- manage Rows-on-top ----
List<T> firstRows = null;
// Decrease limits if firstRowsIds is used
if (rowsOnTopIds != null) {
LOGGER.trace("Prepare rows on top: {}", rowsOnTopIds);
// Coherce row-on-top ids types
Object[] cohercedRowsOnTopId = new Object[rowsOnTopIds.length];
EntityType<? extends T> entityMetamodel = entityManager
.getMetamodel().entity(entity.getType());
// We always have just one id. This id can be an Embedded Id
Class<?> idType = entityMetamodel.getIdType().getJavaType();
@SuppressWarnings("unchecked")
SingularAttribute<? extends T, ?> idAttr = (SingularAttribute<? extends T, ?>) entityMetamodel
.getId(idType);
Object curId;
for (int i = 0; i < rowsOnTopIds.length; i++) {
curId = rowsOnTopIds[i];
if (curId.getClass() != idType) {
cohercedRowsOnTopId[i] = conversionService.convert(curId,
idType);
}
else {
cohercedRowsOnTopId[i] = curId;
}
}
// Create expression for rows-on-top
BooleanExpression firstRowsInExpression = QuerydslUtils
.createCollectionExpression(entity, idAttr.getName(),
Arrays.asList(cohercedRowsOnTopId));
LOGGER.trace("Expression for rowsOnTop: {}", firstRowsInExpression);
// Exclude firstRows from base query
basePredicate = basePredicate.and(firstRowsInExpression.not());
LOGGER.trace("basePredicate to exclude rowsOnTop now is: {}",
basePredicate);
// Gets rows on top
JPAQuery firstRowsQuery = new JPAQuery(entityManager);
firstRowsQuery = firstRowsQuery.from(entity).where(
firstRowsInExpression);
LOGGER.trace("rowsOnTop query is: {}", firstRowsQuery);
try {
// TODO handle fieldSelector
firstRows = firstRowsQuery.list(entity);
}
catch (PersistenceException exSql) {
// Log query
LOGGER.error("Error excecuting SQL for firstRow (sql = '{}' )",
firstRowsQuery);
throw exSql;
}
LOGGER.trace("Found {} rows for rowsOnTop", firstRows.size());
// Adjust limit with rows-on-top found
if (limit != null) {
LOGGER.trace("Update main query limit: {} --> {}", limit, limit
- firstRows.size());
limit = limit - firstRows.size();
}
}
// ----- Execute the query -----
List<T> elements = null;
// Compose the final query and update query var to be used to count
// total amount of rows if needed
if (distinct) {
LOGGER.trace("Use distinct query!!!");
query = query.distinct();
}
// Predicate for base query
boolean hasBasePredicate = true;
if (basePredicate == null) {
basePredicate = new BooleanBuilder();
hasBasePredicate = false;
}
// query projection to count all entities without paging
baseQuery.where(basePredicate);
// query projection to be used to get the results and to count filtered
// results
query = query.where(basePredicate.and(
filtersByColumnPredicate.getValue()).and(
filtersByTablePredicate.getValue()));
// Calculate the total amount of rows taking in account datatables
// search and paging criterias. When results are paginated we
// must execute a count query, otherwise the size of matched rows List
// is the total amount of rows
long totalResultCount = 0;
if (isPaged) {
try {
totalResultCount = query.count();
}
catch (PersistenceException exSql) {
// Log query
LOGGER.error("Error excecuting 'count' SQL: {}", query);
throw exSql;
}
}
if (offset == null) {
offset = new Long(0);
}
else if (offset > totalResultCount) {
// If offset value is bigger than total results,
// offset needs start on 0
offset = new Long(0);
}
// QueryModifiers combines limit and offset
QueryModifiers queryModifiers = new QueryModifiers(limit, offset);
LOGGER.trace("Set limit={} offset={}", limit, offset);
// List ordered and paginated results. An empty list is returned for no
// results.
query = query.orderBy(orderSpecifiersList
.toArray(new OrderSpecifier[orderSpecifiersList.size()]));
LOGGER.debug("Execute query: {}", query);
try {
elements = query.restrict(queryModifiers).list(entity);
}
catch (PersistenceException exSql) {
// Log query
LOGGER.error("Error excecuting SQL: {}", query);
throw exSql;
}
if (!isPaged) {
totalResultCount = elements.size();
}
long totalBaseCount = totalResultCount;
if (hasBasePredicate) {
// Calculate the total amount of entities including base filters
// only
LOGGER.trace("Execute count query: {}", baseQuery);
try {
totalBaseCount = baseQuery.count();
}
catch (PersistenceException exSql) {
// Log query
LOGGER.error("Error excecuting 'count' SQL: {}", baseQuery);
throw exSql;
}
LOGGER.trace("Found : {}", totalBaseCount);
}
if (firstRows != null) {
// Adjust result with rows-on-top
totalResultCount = totalResultCount + firstRows.size();
totalBaseCount = totalBaseCount + firstRows.size();
elements.addAll(0, firstRows);
}
// Create a new SearchResults instance
if (limit == null) {
limit = totalBaseCount;
}
SearchResults<T> searchResults = new SearchResults<T>(elements,
totalResultCount, isPaged, offset, limit, totalBaseCount);
LOGGER.debug(
"findByCriteria: return {} rows from {} (offset={} limit={})",
totalResultCount, totalBaseCount, offset, limit);
return searchResults;
}
/**
* Prepares associationMap for findByCriteria
*
* @param entity
* @param filterByAssociations
* @param datatablesCriterias
* @param findInAllColumns
* @param query
* @param associationMap
* @return
*/
public static <T> JPAQuery prepareQueryAssociationMap(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
DatatablesCriterias datatablesCriterias, boolean findInAllColumns,
JPAQuery query, Map<String, PathBuilder<?>> associationMap) {
LOGGER.debug("Preparing associationMap and joins for entity {}...",
entity.getType());
for (ColumnDef column : datatablesCriterias.getColumnDefs()) {
// true if the search must include this column
boolean findInColumn = StringUtils.isNotEmpty(column.getSearch());
// If no joins given for this column, don't add the JOIN to query
// to improve performance
String associationName = unescapeDot(column.getName());
if (!filterByAssociations.containsKey(associationName)) {
continue;
}
// If column is not sortable and is not filterable, don't add the
// JOIN to query to improve performance
if (!column.isSortable() && !column.isFilterable()) {
continue;
}
// If column is not sortable and no search value provided,
// don't add the JOIN to query to improve performance
if (!column.isSortable() && !findInColumn && !findInAllColumns) {
continue;
}
// Here the column is sortable or it is filterable and column search
// value or all-column search value is provided
PathBuilder<?> associationPath = entity.get(associationName);
query = query.join(associationPath);
// Store join path for later use in where
associationMap.put(associationName, associationPath);
LOGGER.trace("Added join {} -> {} as {}...", entity.getType(),
associationPath, associationName);
}
return query;
}
/**
* Prepares filter part for a query of findByCriteria
*
* @param entity
* @param filterByAssociations
* @param datatablesCriterias
* @param associationMap
* @param filtersByColumnPredicate
* @return
*/
private static <T> BooleanBuilder prepareQueryFilterPart(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
DatatablesCriterias datatablesCriterias,
Map<String, PathBuilder<?>> associationMap,
BooleanBuilder filtersByColumnPredicate,
ConversionService conversionService, MessageSource messageSource) {
// Add filterable columns only
LOGGER.debug("Preparing filter-column expression for entity {}...",
entity.getType());
Predicate filterExpression;
for (ColumnDef column : datatablesCriterias.getColumnDefs()) {
// Each column has its own search by value
String searchStr = column.getSearch();
// true if the search must include this column
boolean findInColumn = column.isFilterable()
&& StringUtils.isNotEmpty(searchStr);
if (findInColumn) {
// Entity field name and type
String fieldName = unescapeDot(column.getName());
LOGGER.trace("Preparing filter for '{}' by '{}'...", fieldName,
searchStr);
// On column search, connect where clauses together by
// AND
// because we want found the records which columns
// match with column filters
filterExpression = QuerydslUtils.createExpression(entity,
fieldName, searchStr, conversionService, messageSource);
filtersByColumnPredicate = filtersByColumnPredicate
.and(filterExpression);
LOGGER.trace("filtersByColumnPredicate AND '{}'",
filterExpression);
// TODO: Este codigo se puede pasar a QuerydslUtils ?
// If column is an association and there are given
// join attributes, add those attributes to WHERE
// predicates
List<String> attributes = filterByAssociations.get(fieldName);
if (attributes != null && attributes.size() > 0) {
// Filters of associated entity properties
BooleanBuilder filtersByAssociationPredicate = new BooleanBuilder();
PathBuilder<?> associationPath = associationMap
.get(fieldName);
List<String> associationFields = filterByAssociations
.get(fieldName);
for (String associationFieldName : associationFields) {
// On association search, connect
// associated entity where clauses by OR
// because all assoc entity properties are
// inside the same column and any of its
// property value can match with given search
// value
filterExpression = QuerydslUtils.createExpression(
associationPath, associationFieldName,
searchStr, conversionService);
filtersByAssociationPredicate = filtersByAssociationPredicate
.or(filterExpression);
LOGGER.trace("filtersByAssociationPredicate OR '{}'",
filterExpression);
}
filtersByColumnPredicate = filtersByColumnPredicate
.and(filtersByAssociationPredicate.getValue());
LOGGER.trace("filtersByColumnPredicate AND '{}'",
filtersByAssociationPredicate.getValue());
}
}
}
LOGGER.debug("Final filtersByColumnPredicate = '{}'",
filtersByColumnPredicate);
return filtersByColumnPredicate;
}
/**
* Prepare search part for a query of findByCriteria
*
* @param entity
* @param filterByAssociations
* @param datatablesCriterias
* @param findInAllColumns
* @param associationMap
* @param filtersByTablePredicate
* @return
*/
private static <T> BooleanBuilder prepareQuerySearchPart(
PathBuilder<T> entity,
Map<String, List<String>> filterByAssociations,
DatatablesCriterias datatablesCriterias, boolean findInAllColumns,
Map<String, PathBuilder<?>> associationMap,
BooleanBuilder filtersByTablePredicate,
ConversionService conversionService) {
String searchStr = datatablesCriterias.getSearch();
if (StringUtils.isEmpty(searchStr)) {
// Nothing to do
return filtersByTablePredicate;
}
LOGGER.debug(
"Preparing search expression for '{}' string on entity {}...",
searchStr, entity.getType());
if (findInAllColumns) {
boolean expressionExists = false;
// Add filterable columns only
for (ColumnDef column : datatablesCriterias.getColumnDefs()) {
if (column.isFilterable()) {
// Entity field name and type
String fieldName = unescapeDot(column.getName());
LOGGER.trace("Check expression column {}...", fieldName);
// Find in all columns means we want to find given
// value in at least one entity property, so we must
// join the where clauses by OR
Predicate expression = QuerydslUtils.createExpression(
entity, fieldName, searchStr, conversionService);
if (expression != null) {
filtersByTablePredicate = filtersByTablePredicate
.or(expression);
LOGGER.trace("Added expression {}", expression);
expressionExists = true;
}
// If column is an association and there are given
// join attributes, add those attributes to WHERE
// predicates
List<String> attributes = filterByAssociations
.get(fieldName);
if (attributes != null && attributes.size() > 0) {
PathBuilder<?> associationPath = associationMap
.get(fieldName);
List<String> associationFields = filterByAssociations
.get(fieldName);
for (String associationFieldName : associationFields) {
expression = QuerydslUtils.createExpression(
associationPath, associationFieldName,
searchStr, conversionService);
filtersByTablePredicate = filtersByTablePredicate
.or(expression);
LOGGER.trace(
"Added expression (by association) {}",
expression);
}
}
}
}
// If expression is null returns error to returns an empty
// DataSource
if (!expressionExists) {
throw new RuntimeException("Expression cannot be null");
}
}
LOGGER.debug("Search expression: {}", filtersByTablePredicate);
return filtersByTablePredicate;
}
/**
* prepares order part for a query of findByCriteria
*
* @param entity
* @param orderByAssociations
* @param datatablesCriterias
* @param associationMap
* @return
*/
@SuppressWarnings("unchecked")
private static <E extends Comparable<?>, T> List<OrderSpecifier<?>> prepareQueryOrder(
PathBuilder<T> entity,
Map<String, List<String>> orderByAssociations,
DatatablesCriterias datatablesCriterias,
Map<String, PathBuilder<?>> associationMap) {
List<OrderSpecifier<?>> orderSpecifiersList = new ArrayList<OrderSpecifier<?>>();
if (datatablesCriterias.hasOneSortedColumn()) {
LOGGER.debug("Preparing order for entity {}", entity.getType());
OrderSpecifier<?> queryOrder;
for (ColumnDef column : datatablesCriterias.getSortingColumnDefs()) {
// If column is not sortable, don't add it to order by clauses
if (!column.isSortable()) {
continue;
}
// If no sort direction provided, don't add this column to
// order by clauses
if (column.getSortDirection() == null) {
LOGGER.debug("Column {} ignored: not sortDirection",
column.getName());
continue;
}
// Convert Datatables sort direction to Querydsl order
Order order = Order.DESC;
if (column.getSortDirection() == SortDirection.ASC) {
order = Order.ASC;
}
// Entity field name and type. Type must extend Comparable
// interface
String fieldName = unescapeDot(column.getName());
LOGGER.trace("Adding column {} {}...", fieldName, order);
Class<E> fieldType = (Class<E>) QuerydslUtils.getFieldType(
fieldName, entity);
List<String> attributes = orderByAssociations.get(fieldName);
try {
// If column is an association and there are given
// order by attributes, add those attributes to ORDER BY
// clauses
if (attributes != null && attributes.size() > 0) {
PathBuilder<?> associationPath = associationMap
.get(fieldName);
List<String> associationFields = orderByAssociations
.get(fieldName);
for (String associationFieldName : associationFields) {
// Get associated entity field type
Class<E> associationFieldType = (Class<E>) BeanUtils
.findPropertyType(
associationFieldName,
ArrayUtils
.<Class<?>> toArray(fieldType));
queryOrder = QuerydslUtils.createOrderSpecifier(
associationPath, associationFieldName,
associationFieldType, order);
orderSpecifiersList.add(queryOrder);
LOGGER.trace("Added order: {}", queryOrder);
}
}
// Otherwise column is an entity property
else {
queryOrder = QuerydslUtils.createOrderSpecifier(entity,
fieldName, fieldType, order);
orderSpecifiersList.add(queryOrder);
LOGGER.trace("Added order: {}", queryOrder);
}
}
catch (ClassCastException ex) {
// Do nothing, on class cast exception order specifier will
// be null
LOGGER.debug("CastException preparing order for entity {}",
entity.getType(), ex);
continue;
}
catch (Exception ex) {
LOGGER.warn("Exception preparing order for entity {}",
entity.getType(), ex);
continue;
}
}
}
return orderSpecifiersList;
}
/**
* Populate a {@link DataSet} from given entity list.
* <p/>
* Field values will be converted to String using given
* {@link ConversionService} and Date fields will be converted to Date using
* {@link DateFormat} with given date patterns.
*
* @param entities List of T entities to convert to Datatables data
* @param pkFieldName The T entity field that contains the PK
* @param totalRecords Total amount of records
* @param totalDisplayRecords Amount of records found
* @param columns {@link ColumnDef} list
* @param datePatterns Patterns to convert Date fields to String. The Map
* contains one pattern for each entity Date field keyed by field
* name. For Roo compatibility the key could follow the pattern
* {@code uncapitalize( ENTITY ) + "_" + lower_case( FIELD ) + "_date_format"}
* too
* @param conversionService
* @return
*/
public static <T> DataSet<Map<String, String>> populateDataSet(
List<T> entities, String pkFieldName, long totalRecords,
long totalDisplayRecords, List<ColumnDef> columns,
Map<String, Object> datePatterns,
ConversionService conversionService) {
// Check arguments aren't null
Assert.notNull(pkFieldName);
Assert.notNull(columns);
Assert.notNull(conversionService);
// Map of data rows
List<Map<String, String>> rows = new ArrayList<Map<String, String>>(
entities.size());
if (CollectionUtils.isEmpty(entities)) {
return new DataSet<Map<String, String>>(rows, 0l, 0l);
}
// If null, create empty Map to avoid control code overload
if (CollectionUtils.isEmpty(datePatterns)) {
datePatterns = new HashMap<String, Object>();
}
Map<String, SimpleDateFormat> dateFormatters = new HashMap<String, SimpleDateFormat>(
datePatterns.size());
// Prepare required fields
Set<String> fields = new HashSet<String>();
fields.add(pkFieldName);
// Add fields from request
for (ColumnDef colum : columns) {
fields.add(colum.getName());
}
BeanWrapperImpl entityBean = null;
String valueStr = null;
// Populate each row, note a row is a Map containing
// fieldName = fieldValue
for (T entity : entities) {
Map<String, String> row = new HashMap<String, String>(fields.size());
if (entityBean == null) {
entityBean = new BeanWrapperImpl(entity);
}
else {
entityBean.setWrappedInstance(entity);
}
for (String fieldName : fields) {
String unescapedFieldName = unescapeDot(fieldName);
// check if property exists (trace it else)
if (!entityBean.isReadableProperty(unescapedFieldName)) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Property [{}] not fond in bean {} [{}]",
unescapedFieldName, entity.getClass()
.getSimpleName(), entity);
}
row.put(fieldName, "");
continue;
}
// Convert field value to string
valueStr = convertFieldValueToString(datePatterns,
dateFormatters, conversionService, entityBean, entity,
fieldName, unescapedFieldName);
row.put(fieldName, valueStr);
// Set PK value as DT_RowId
// Note when entity has composite PK Roo generates the need
// convert method and adds it to ConversionService, so
// when processed field is the PK the valueStr is the
// composite PK instance marshalled to JSON notation and
// Base64 encoded
if (pkFieldName.equalsIgnoreCase(fieldName)) {
row.put("DT_RowId", valueStr);
}
}
rows.add(row);
}
DataSet<Map<String, String>> dataSet = new DataSet<Map<String, String>>(
rows, totalRecords, totalDisplayRecords);
return dataSet;
}
/**
* Convert a field value to string
*
* @param datePatterns
* @param dateFormatters
* @param conversionService
* @param entityBean
* @param entity
* @param fieldName
* @param unescapedFieldName
* @return
*/
private static <T> String convertFieldValueToString(
Map<String, Object> datePatterns,
Map<String, SimpleDateFormat> dateFormatters,
ConversionService conversionService, BeanWrapperImpl entityBean,
T entity, String fieldName, String unescapedFieldName) {
try {
Object value = null;
TypeDescriptor fieldDesc = entityBean
.getPropertyTypeDescriptor(unescapedFieldName);
TypeDescriptor strDesc = TypeDescriptor.valueOf(String.class);
value = entityBean.getPropertyValue(unescapedFieldName);
if (value == null) {
return "";
}
// For dates
if (Date.class.isAssignableFrom(value.getClass())
|| Calendar.class.isAssignableFrom(value.getClass())) {
SimpleDateFormat formatter = getDateFormatter(datePatterns,
dateFormatters, entityBean.getWrappedClass(),
unescapedFieldName);
if (formatter != null) {
if (Calendar.class.isAssignableFrom(value.getClass())) {
// Gets Date instance as SimpleDateFormat
// doesn't works with Calendar
value = ((Calendar) value).getTime();
}
return formatter.format(value);
}
}
String stringValue;
// Try to use conversion service (uses field descrition
// to handle field format annotations)
if (conversionService.canConvert(fieldDesc, strDesc)) {
stringValue = (String) conversionService.convert(value,
fieldDesc, strDesc);
if (stringValue == null) {
stringValue = "";
}
}
else {
stringValue = ObjectUtils.getDisplayString(value);
}
return stringValue;
}
catch (Exception ex) {
LOGGER.error(String.format(
"Error getting value of property [%s] in bean %s [%s]",
unescapedFieldName,
entity.getClass().getSimpleName(),
org.apache.commons.lang3.ObjectUtils.firstNonNull(
entity.toString(), "{unknow}")), ex);
return "";
}
}
/**
* Get Date formatter by field name
* <p/>
* If no pattern found, try standard Roo key
* {@code uncapitalize( ENTITY ) + "_" + lower_case( FIELD ) + "_date_format"}
*
* @param datePatterns Contains field name and related data pattern
* @param entityClass Entity class to which the field belong to
* @param fieldName Field to search pattern
* @return
*/
private static SimpleDateFormat getDateFormatter(
Map<String, Object> datePatterns,
Map<String, SimpleDateFormat> dateFormatters, Class<?> entityClass,
String fieldName) {
SimpleDateFormat result = null;
String lowerCaseFieldName = fieldName.toLowerCase();
result = dateFormatters.get(lowerCaseFieldName);
if (result != null) {
return result;
}
else if (dateFormatters.containsKey(lowerCaseFieldName)) {
return null;
}
// Get pattern by field name
String pattern = (String) datePatterns.get(lowerCaseFieldName);
if (StringUtils.isEmpty(pattern)) {
// Try to get the name of entity class (without javassit suffix)
String baseClass = StringUtils.substringBefore(
entityClass.getSimpleName(), "$");// );"_$");
// try to get pattern by Roo key
String rooKey = StringUtils.uncapitalize(baseClass).concat("_")
.concat(lowerCaseFieldName).concat("_date_format");
pattern = (String) datePatterns.get(rooKey);
}
if (!StringUtils.isEmpty(pattern)) {
result = new SimpleDateFormat(pattern);
}
dateFormatters.put(lowerCaseFieldName, result);
return result;
}
/**
* Constructs the {@code HtmlTable} used to export the data.
* <p />
* It uses the parameters of the request to check if the column is
* exportable or not, these parameters are named:
* <ul>
* <li>{@code [export_type_extension]ExportColumns}, where
* <emp>[export_type_extension]</emp> is the extension of the format to
* export, for example: {@code csvExportColumns}</li>
* <li>{@code allExportColumns}</li>
* </ul>
* <p />
* Also uses the parameter {@code columnsTitle} to indicate the title of
* each column, this parameter has as value a {@code String} with the format
* of a Map as follows:
*
* <pre>
* {property1||value1, property2||value2, ... , propertyN||valueN}
* </pre>
*
* @param data the data to make the {@code HtmlTable}.
* @param criterias the {@code DatatablesCriterias}.
* @param exportConf the {@code ExportConf}.
* @param request the {@code HttpServletRequest}.
* @return the {@code HtmlTable} used to export the data.
*/
public static HtmlTable makeHtmlTable(List<Map<String, String>> data,
DatatablesCriterias criterias, ExportConf exportConf,
HttpServletRequest request) {
ColumnStep tableBuilder = new HtmlTableBuilder<Map<String, String>>()
.newBuilder("tableId", data, request);
// Obtain exportable columns
String exportTypeExtension = StringUtils.lowerCase(exportConf.getType()
.getExtension());
String thisFormatExportColumnsStr = request
.getParameter(exportTypeExtension.concat("ExportColumns"));
if (StringUtils.isEmpty(thisFormatExportColumnsStr)) {
thisFormatExportColumnsStr = "";
}
String allFormatExportColumnsStr = request
.getParameter("allExportColumns");
if (StringUtils.isEmpty(allFormatExportColumnsStr)) {
allFormatExportColumnsStr = "";
}
List<String> thisFormatExporColumns = Arrays.asList(StringUtils.split(
thisFormatExportColumnsStr, ","));
List<String> allFormatExportColumns = Arrays.asList(StringUtils.split(
allFormatExportColumnsStr, ","));
BeforeEndStep columns = null;
if (!allFormatExportColumns.isEmpty()
|| !thisFormatExporColumns.isEmpty()) {
// Obtain the column titles
Map<String, String> columnsTitleMap = new HashMap<String, String>();
String columnsTitleStr = request.getParameter("columnsTitle");
columnsTitleStr = StringUtils.substring(columnsTitleStr, 1,
(columnsTitleStr.length() - 1));
List<String> columnsTitleList = Arrays.asList(StringUtils.split(
columnsTitleStr, ","));
for (String columnsTitle : columnsTitleList) {
String[] columsTitleArray = StringUtils.split(columnsTitle,
"||");
if (columsTitleArray.length == 2) {
columnsTitleMap.put(columsTitleArray[0].trim(),
columsTitleArray[1].trim());
}
}
List<ColumnDef> columnDefs = criterias.getColumnDefs();
for (ColumnDef columnDef : columnDefs) {
String columnProperty = columnDef.getName();
if (allFormatExportColumns.contains(columnProperty)
|| thisFormatExporColumns.contains(columnProperty)) {
String columnTitle = columnsTitleMap.get(columnProperty);
if (StringUtils.isBlank(columnTitle)) {
columnTitle = columnProperty;
}
columnTitle = StringUtils.replace(columnTitle, "~~", ",");
columns = tableBuilder.column()
.fillWithProperty(columnProperty)
.title(columnTitle);
}
}
}
if (columns == null) {
columns = tableBuilder.column().fillWithProperty("-").title("---");
}
return columns.configureExport(exportConf).build();
}
/**
* Unescapes the string to represent it with dot ".", that is, the default
* character used to indicate attributes of an object (p.e. "user.name")
*
* @param str the string to unescape.
* @return the string unescaped.
*/
private static String unescapeDot(String str) {
return str.replace(SEPARATOR_FIELDS_ESCAPED, SEPARATOR_FIELDS);
}
/**
*
* Check if filter expression is correct for the input type
*
* @param type
* @param expression
* @param messageSource
* @return
*/
public static boolean checkFilterExpressions(Class<?> type,
String expression, MessageSource messageSource) {
// By default filter is not correct
// Checking String filters
if (String.class == type) {
return checkStringFilters(expression, messageSource);
}
else if (Boolean.class == type || boolean.class == type) {
return checkBooleanFilters(expression, messageSource);
}
else if (Number.class.isAssignableFrom(type)
|| QuerydslUtils.NUMBER_PRIMITIVES.contains(type)) {
return checkNumericFilters(expression, messageSource);
}
else if (Date.class.isAssignableFrom(type)
|| Calendar.class.isAssignableFrom(type)) {
return checkDateFilters(expression, messageSource);
}
return false;
}
public static boolean checkStringFilters(String expression,
MessageSource messageSource) {
// All operations
String endsOperation = "ENDS";
String startsOperation = "STARTS";
String containsOperation = "CONTAINS";
String isEmptyOperation = "ISEMPTY";
String isNotEmptyOperation = "ISNOTEMPTY";
String isNullOperation = ISNULL_OPE;
String isNotNullOperation = NOTNULL_OPE;
if (messageSource != null) {
endsOperation = messageSource.getMessage(
"global.filters.operations.string.ends", null,
LocaleContextHolder.getLocale());
startsOperation = messageSource.getMessage(
"global.filters.operations.string.starts", null,
LocaleContextHolder.getLocale());
containsOperation = messageSource.getMessage(
"global.filters.operations.string.contains", null,
LocaleContextHolder.getLocale());
isEmptyOperation = messageSource.getMessage(
"global.filters.operations.string.isempty", null,
LocaleContextHolder.getLocale());
isNotEmptyOperation = messageSource.getMessage(
"global.filters.operations.string.isnotempty", null,
LocaleContextHolder.getLocale());
isNullOperation = messageSource.getMessage(G_ISNULL_OPE, null,
LocaleContextHolder.getLocale());
isNotNullOperation = messageSource.getMessage(G_NOTNULL_OPE, null,
LocaleContextHolder.getLocale());
}
// If written expression is ENDS operation
Pattern endsOperator = Pattern.compile(String.format(
"%s[(]([a-zA-Z\\s\\d]*)[)]", endsOperation));
Matcher endsMatcher = endsOperator.matcher(expression);
if (endsMatcher.matches()) {
return true;
}
// If written expression is STARTS operation
Pattern startsOperator = Pattern.compile(String.format("%s[(](.+)[)]$",
startsOperation));
Matcher startsMatcher = startsOperator.matcher(expression);
if (startsMatcher.matches()) {
return true;
}
// If written expression is CONTAINS operation
Pattern containsOperator = Pattern.compile(String.format(
"%s[(](.+)[)]$", containsOperation));
Matcher containsMatcher = containsOperator.matcher(expression);
if (containsMatcher.matches()) {
return true;
}
// If written expression is ISEMPTY operation
Pattern isEmptyOperator = Pattern.compile(String.format("%s",
isEmptyOperation));
Matcher isEmptyMatcher = isEmptyOperator.matcher(expression);
if (isEmptyMatcher.matches()) {
return true;
}
// If written expression is ISNOTEMPTY operation
Pattern isNotEmptyOperator = Pattern.compile(String.format("%s",
isNotEmptyOperation));
Matcher isNotEmptyMatcher = isNotEmptyOperator.matcher(expression);
if (isNotEmptyMatcher.matches()) {
return true;
}
// If written expression is ISNULL operation
Pattern isNullOperator = Pattern.compile(String.format("%s",
isNullOperation));
Matcher isNullMatcher = isNullOperator.matcher(expression);
if (isNullMatcher.matches()) {
return true;
}
// If written expression is ISNOTNULL operation
Pattern isNotNullOperator = Pattern.compile(String.format("%s",
isNotNullOperation));
Matcher isNotNullMatcher = isNotNullOperator.matcher(expression);
if (isNotNullMatcher.matches()) {
return true;
}
// If written expression is a symbol operation expression
// Getting expressions with symbols
Pattern symbolOperator = Pattern.compile("[=]?(.+)");
Matcher symbolMatcher = symbolOperator.matcher(expression);
if (symbolMatcher.matches()) {
return true;
}
return false;
}
public static boolean checkBooleanFilters(String expression,
MessageSource messageSource) {
// Getting all operations
String trueOperation = "TRUE";
String falseOperation = "FALSE";
String isNullOperation = ISNULL_OPE;
String isNotNullOperation = NOTNULL_OPE;
if (messageSource != null) {
trueOperation = messageSource.getMessage(
"global.filters.operations.boolean.true", null,
LocaleContextHolder.getLocale());
falseOperation = messageSource.getMessage(
"global.filters.operations.boolean.false", null,
LocaleContextHolder.getLocale());
isNullOperation = messageSource.getMessage(G_ISNULL_OPE, null,
LocaleContextHolder.getLocale());
isNotNullOperation = messageSource.getMessage(G_NOTNULL_OPE, null,
LocaleContextHolder.getLocale());
}
// If written function is TRUE
Pattern trueOperator = Pattern.compile(String.format("%s",
trueOperation));
Matcher trueMatcher = trueOperator.matcher(expression);
if (trueMatcher.matches()) {
return true;
}
// If written function is FALSE
Pattern falseOperator = Pattern.compile(String.format("%s",
falseOperation));
Matcher falseMatcher = falseOperator.matcher(expression);
if (falseMatcher.matches()) {
return true;
}
// If written expression is ISNULL operation
Pattern isNullOperator = Pattern.compile(String.format("%s",
isNullOperation));
Matcher isNullMatcher = isNullOperator.matcher(expression);
if (isNullMatcher.matches()) {
return true;
}
// If written expression is ISNOTNULL operation
Pattern isNotNullOperator = Pattern.compile(String.format("%s",
isNotNullOperation));
Matcher isNotNullMatcher = isNotNullOperator.matcher(expression);
if (isNotNullMatcher.matches()) {
return true;
}
return false;
}
public static boolean checkNumericFilters(String expression,
MessageSource messageSource) {
if (NumberUtils.isNumber(expression)) {
return true;
}
else {
// Getting expressions with symbols
Pattern symbolOperator = Pattern
.compile("([!=><][=>]?)([-]?[\\d.,]*)");
Matcher symbolMatcher = symbolOperator.matcher(expression);
if (symbolMatcher.matches()) {
String symbolExpression = symbolMatcher.group(1);
String value = symbolMatcher.group(2);
if (!StringUtils.isBlank(value)) {
if (symbolExpression.equals("=")
|| symbolExpression.equals("==")) {
return true;
}
else if (symbolExpression.equals(">")
|| symbolExpression.equals(">>")) {
return true;
}
else if (symbolExpression.equals("<")) {
return true;
}
else if (symbolExpression.equals(">=")) {
return true;
}
else if (symbolExpression.equals("<=")) {
return true;
}
else if (symbolExpression.equals("!=")
|| symbolExpression.equals("<>")) {
return true;
}
}
}
// Get all operations
String isNullOperation = ISNULL_OPE;
String isNotNullOperation = NOTNULL_OPE;
String betweenOperation = "BETWEEN";
if (messageSource != null) {
isNullOperation = messageSource.getMessage(G_ISNULL_OPE, null,
LocaleContextHolder.getLocale());
isNotNullOperation = messageSource.getMessage(G_NOTNULL_OPE,
null, LocaleContextHolder.getLocale());
betweenOperation = messageSource.getMessage(
"global.filters.operations.number.between", null,
LocaleContextHolder.getLocale());
}
// If written function is BETWEEN function
Pattern betweenFunctionOperator = Pattern.compile(String.format(
"%s[(]([-]?[\\d.,]*);([-]?[\\d.,]*)[)]", betweenOperation));
Matcher betweenFunctionMatcher = betweenFunctionOperator
.matcher(expression);
if (betweenFunctionMatcher.matches()) {
// Getting valueFrom and valueTo
String valueFrom = betweenFunctionMatcher.group(1);
String valueTo = betweenFunctionMatcher.group(2);
if (!StringUtils.isBlank(valueFrom)
&& !StringUtils.isBlank(valueTo)) {
return true;
}
}
// If written expression is ISNULL operation
Pattern isNullOperator = Pattern.compile(String.format("%s",
isNullOperation));
Matcher isNullMatcher = isNullOperator.matcher(expression);
if (isNullMatcher.matches()) {
return true;
}
// If written expression is ISNOTNULL operation
Pattern isNotNullOperator = Pattern.compile(String.format("%s",
isNotNullOperation));
Matcher isNotNullMatcher = isNotNullOperator.matcher(expression);
if (isNotNullMatcher.matches()) {
return true;
}
}
return false;
}
public static boolean checkDateFilters(String expression,
MessageSource messageSource) {
// All possible operations
String date = "DATE";
String year = "YEAR";
String month = "MONTH";
String day = "DAY";
String between = "BETWEEN";
String isNullOperation = ISNULL_OPE;
String isNotNullOperation = NOTNULL_OPE;
String datePattern = "dd/MM/yyyy";
if (messageSource != null) {
date = messageSource.getMessage(
"global.filters.operations.date.date", null,
LocaleContextHolder.getLocale());
year = messageSource.getMessage(
"global.filters.operations.date.year", null,
LocaleContextHolder.getLocale());
month = messageSource.getMessage(
"global.filters.operations.date.month", null,
LocaleContextHolder.getLocale());
day = messageSource.getMessage(
"global.filters.operations.date.day", null,
LocaleContextHolder.getLocale());
between = messageSource.getMessage(
"global.filters.operations.date.between", null,
LocaleContextHolder.getLocale());
isNullOperation = messageSource.getMessage(G_ISNULL_OPE, null,
LocaleContextHolder.getLocale());
isNotNullOperation = messageSource.getMessage(G_NOTNULL_OPE, null,
LocaleContextHolder.getLocale());
datePattern = messageSource.getMessage(
"global.filters.operations.date.pattern", null,
LocaleContextHolder.getLocale());
}
// Getting simpleDateFormat
DateFormat dateFormat = new SimpleDateFormat(datePattern);
// If written expression is ISNULL operation
Pattern isNullOperator = Pattern.compile(String.format("%s",
isNullOperation));
Matcher isNullMatcher = isNullOperator.matcher(expression);
if (isNullMatcher.matches()) {
return true;
}
// If written expression is ISNOTNULL operation
Pattern isNotNullOperator = Pattern.compile(String.format("%s",
isNotNullOperation));
Matcher isNotNullMatcher = isNotNullOperator.matcher(expression);
if (isNotNullMatcher.matches()) {
return true;
}
// Creating regex to get DATE operator
Pattern dateOperator = Pattern.compile(String.format(
"%s[(]([\\d\\/]*)[)]", date));
Matcher dateMatcher = dateOperator.matcher(expression);
if (dateMatcher.matches()) {
try {
String dateValue = dateMatcher.group(1);
Date dateToFilter = dateFormat.parse(dateValue);
Calendar searchCal = Calendar.getInstance();
searchCal.setTime(dateToFilter);
return true;
}
catch (ParseException e) {
return false;
}
}
// Creating regex to get YEAR operator
Pattern yearOperator = Pattern.compile(String.format(
"%s[(]([\\d]*)[)]", year));
Matcher yearMatcher = yearOperator.matcher(expression);
if (yearMatcher.matches()) {
return true;
}
// Creating regex to get MONTH operator
Pattern monthOperator = Pattern.compile(String.format(
"%s[(]([\\d]*)[)]", month));
Matcher monthMatcher = monthOperator.matcher(expression);
if (monthMatcher.matches()) {
return true;
}
// Creating regex to get DAY operator
Pattern dayOperator = Pattern.compile(String.format("%s[(]([\\d]*)[)]",
day));
Matcher dayMatcher = dayOperator.matcher(expression);
if (dayMatcher.matches()) {
return true;
}
// Creating regex to get BETWEEN operator
Pattern betweenOperator = Pattern.compile(String.format(
"%s[(]([\\d\\/]*);([\\d\\/]*)[)]", between));
Matcher betweenMatcher = betweenOperator.matcher(expression);
if (betweenMatcher.matches()) {
String valueFrom = betweenMatcher.group(1);
String valueTo = betweenMatcher.group(2);
if (StringUtils.isNotBlank(valueFrom)
&& StringUtils.isNotBlank(valueTo)) {
try {
Date dateFrom = dateFormat.parse(valueFrom);
Date dateTo = dateFormat.parse(valueTo);
Calendar dateFromCal = Calendar.getInstance();
dateFromCal.setTime(dateFrom);
Calendar dateToCal = Calendar.getInstance();
dateToCal.setTime(dateTo);
return true;
}
catch (Exception e) {
return false;
}
}
}
return false;
}
}