/* * gvNIX. Spring Roo based RAD tool for Generalitat Valenciana * Copyright (C) 2013 Generalitat Valenciana * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gvnix.web.datatables.util.impl; 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.gvnix.web.datatables.util.DatatablesUtilsBean; import org.gvnix.web.datatables.util.EntityManagerProvider; import org.gvnix.web.datatables.util.QuerydslUtilsBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.factory.annotation.Autowired; 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; /** * Default Datatables utility service implementation * * @author gvNIX team */ public class DatatablesUtilsBeanImpl implements DatatablesUtilsBean { // Logger private static Logger LOGGER = LoggerFactory .getLogger(DatatablesUtilsBeanImpl.class); @Autowired private ConversionService conversionService; @Autowired private MessageSource messageSource; @Autowired private EntityManagerProvider entityManagerProvider; @Autowired private QuerydslUtilsBean querydslUtilsBean; /** * {@inheritDoc} */ @Override public JPAQuery newJPAQuery(EntityManager em) { return new JPAQuery(em); } /** * {@inheritDoc} */ @Override public boolean isSpecialFilterParameters(String name) { if (name.startsWith(QuerydslUtilsBean.OPERATOR_PREFIX)) { return true; } else if (DatatablesUtilsBean.ROWS_ON_TOP_IDS_PARAM.equals(name)) { return true; } return false; } /** * {@inheritDoc} */ @Override public <T> SearchResults<T> findByCriteria(Class<T> entityClass, DatatablesCriterias datatablesCriterias) { return findByCriteria(entityClass, datatablesCriterias); } /** * {@inheritDoc} */ @Override public <T> SearchResults<T> findByCriteria(Class<T> entityClass, DatatablesCriterias datatablesCriterias, Map<String, Object> baseSearchValuesMap) { return findByCriteria(entityClass, null, null, datatablesCriterias, baseSearchValuesMap, false); } /** * {@inheritDoc} */ @Override public <T> SearchResults<T> findByCriteria(Class<T> entityClass, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias) { return findByCriteria(entityClass, filterByAssociations, orderByAssociations, datatablesCriterias); } /** * {@inheritDoc} */ @Override public <T> SearchResults<T> findByCriteria(Class<T> entityClass, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, Map<String, Object> baseSearchValuesMap) { return findByCriteria(entityClass, filterByAssociations, orderByAssociations, datatablesCriterias, baseSearchValuesMap, false); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( Class<T> entityClass, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, Map<String, Object> baseSearchValuesMap, boolean distinct) 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 = querydslUtilsBean.createPredicateByAnd(entity, newBaseSearch); } else { basePredicate = querydslUtilsBean.createPredicateByAnd(entity, baseSearchValuesMap); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("findByCriteria baseSearch by map-of-values: {}", basePredicate.toString()); } } else { basePredicate = new BooleanBuilder(); } return findByCriteria(entityClass, filterByAssociations, orderByAssociations, datatablesCriterias, basePredicate, distinct, rowsOnTopIds); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( Class<T> entityClass, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate, boolean distinct) throws IllegalArgumentException { Assert.notNull(entityClass); // Query DSL builder PathBuilder<T> entity = new PathBuilder<T>(entityClass, "entity"); return findByCriteria(entity, filterByAssociations, orderByAssociations, datatablesCriterias, basePredicate, distinct, null); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( Class<T> entityClass, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate, boolean distinct, Object[] rowsOnTopIds) throws IllegalArgumentException { Assert.notNull(entityClass); // Query DSL builder PathBuilder<T> entity = new PathBuilder<T>(entityClass, "entity"); return findByCriteria(entity, filterByAssociations, orderByAssociations, datatablesCriterias, basePredicate, distinct, rowsOnTopIds); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( PathBuilder<T> entity, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate) throws IllegalArgumentException { return findByCriteria(entity, filterByAssociations, orderByAssociations, datatablesCriterias, basePredicate, false, null); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( PathBuilder<T> entity, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate) throws IllegalArgumentException { return findByCriteria(entity, null, null, datatablesCriterias, basePredicate, false, null); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( PathBuilder<T> entity, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate, Object[] rowsOnTopIds) throws IllegalArgumentException { return findByCriteria(entity, null, null, datatablesCriterias, basePredicate, false, rowsOnTopIds); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( PathBuilder<T> entity, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate, boolean distinct) throws IllegalArgumentException { return findByCriteria(entity, null, null, datatablesCriterias, basePredicate, false, null); } /** * {@inheritDoc} */ @Override public <T, E extends Comparable<?>> SearchResults<T> findByCriteria( PathBuilder<T> entity, Map<String, List<String>> filterByAssociations, Map<String, List<String>> orderByAssociations, DatatablesCriterias datatablesCriterias, BooleanBuilder basePredicate, boolean distinct, Object[] rowsOnTopIds) throws IllegalArgumentException { // Check arguments aren't null EntityManager entityManager = entityManagerProvider .getEntityManager(entity.getType()); 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 = newJPAQuery(entityManager); query = query.from(entity); // baseQuery will use base search values only in order to count // all for success paging JPAQuery baseQuery = newJPAQuery(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); } // Build the query to search the given value in all columns filtersByTablePredicate = prepareQuerySearchPart(entity, filterByAssociations, datatablesCriterias, findInAllColumns, associationMap, filtersByTablePredicate); } 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 = querydslUtilsBean .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 = newJPAQuery(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; } /** * {@inheritDoc} */ @Override public <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 <T> BooleanBuilder prepareQueryFilterPart(PathBuilder<T> entity, Map<String, List<String>> filterByAssociations, DatatablesCriterias datatablesCriterias, Map<String, PathBuilder<?>> associationMap, BooleanBuilder filtersByColumnPredicate) { // 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 = querydslUtilsBean.createFilterExpression( entity, fieldName, searchStr); 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 = querydslUtilsBean .createFilterExpression(associationPath, associationFieldName, searchStr); 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 <T> BooleanBuilder prepareQuerySearchPart(PathBuilder<T> entity, Map<String, List<String>> filterByAssociations, DatatablesCriterias datatablesCriterias, boolean findInAllColumns, Map<String, PathBuilder<?>> associationMap, BooleanBuilder filtersByTablePredicate) { 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 = querydslUtilsBean .createSearchExpression(entity, fieldName, searchStr); 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 = querydslUtilsBean .createSearchExpression(associationPath, associationFieldName, searchStr); 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 <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>) querydslUtilsBean.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 = querydslUtilsBean .createOrderSpecifier(associationPath, associationFieldName, associationFieldType, order); orderSpecifiersList.add(queryOrder); LOGGER.trace("Added order: {}", queryOrder); } } // Otherwise column is an entity property else { queryOrder = querydslUtilsBean.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; } /** * {@inheritDoc} */ @Override public <T> DataSet<Map<String, String>> populateDataSet(List<T> entities, String pkFieldName, long totalRecords, long totalDisplayRecords, List<ColumnDef> columns, Map<String, Object> datePatterns) { // 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, 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 <T> String convertFieldValueToString( Map<String, Object> datePatterns, Map<String, SimpleDateFormat> dateFormatters, 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 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; } /** * {@inheritDoc} */ @Override public 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 String unescapeDot(String str) { return str.replace(SEPARATOR_FIELDS_ESCAPED, SEPARATOR_FIELDS); } /** * {@inheritDoc} */ @Override public boolean checkFilterExpressions(Class<?> type, String expression) { // By default filter is not correct // Checking String filters if (String.class == type) { return checkStringFilters(expression); } else if (Boolean.class == type || boolean.class == type) { return checkBooleanFilters(expression); } else if (Number.class.isAssignableFrom(type) || QuerydslUtilsBean.NUMBER_PRIMITIVES.contains(type)) { return checkNumericFilters(expression); } else if (Date.class.isAssignableFrom(type) || Calendar.class.isAssignableFrom(type)) { return checkDateFilters(expression); } return false; } @Override public boolean checkStringFilters(String expression) { // 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; } @Override public boolean checkBooleanFilters(String expression) { // 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; } @Override public boolean checkNumericFilters(String expression) { 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; } @Override public boolean checkDateFilters(String expression) { // 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; } }