/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.backend.action.persistence.hibernate; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.Session; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Disjunction; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.criterion.Subqueries; import org.hibernate.transform.ResultTransformer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jspresso.framework.application.backend.action.AbstractQueryComponentsAction; import org.jspresso.framework.application.backend.persistence.hibernate.HibernateBackendController; import org.jspresso.framework.model.component.IQueryComponent; import org.jspresso.framework.model.entity.IEntity; import org.jspresso.framework.model.persistence.hibernate.criterion.EnhancedDetachedCriteria; import org.jspresso.framework.model.persistence.hibernate.criterion.ICriteriaFactory; import org.jspresso.framework.util.accessor.IAccessor; import org.jspresso.framework.util.accessor.IAccessorFactory; /** * This action is used to Hibernate query entities by example. It is used behind * the scene in several places in Jspresso based applications, as in filtered * bean collection modules, list of values, ... The principles are to tailor an * Hibernate Criterion based on the Jspresso "{@code IQueryComponent} * ". A Jspresso query component is a hierarchical data structure that * mimics a portion of the domain model headed by an entity. It is essentially a * set of property/value pairs where values can be : * <ol> * <li>a scalar value</li> * <li>a comparable query structure (operator, inf and sup value) to place a * constraint on a comparable property (date, number, ...)</li> * <li>a sub query component</li> * </ol> * <p/> * Out of this query component, the action will build an Hibernate detached * criteria by constructing all join sub-criteria whenever necessary. * <p/> * Once the detached criteria is complete, the action will perform the Hibernate * query while using paging information taken from the query component as well * as custom sorting properties. * <p/> * Whenever the query is successful, the result is merged back to the * application session and assigned to the query component * {@code queriedComponents} property. * <p/> * Note that there are 2 hooks that can be configured by injection to fine-tune * the performed query : {@code queryComponentRefiner} and * {@code criteriaRefiner}. * * @author Vincent Vandenschrick * @version $LastChangedRevision : 9166 $ */ public class QueryEntitiesAction extends AbstractQueryComponentsAction { private static final Logger LOG = LoggerFactory.getLogger(QueryEntitiesAction.class); private static final String CRITERIA_FACTORY = "CRITERIA_FACTORY"; private static final String CRITERIA_REFINER = "CRITERIA_REFINER"; private ICriteriaFactory criteriaFactory; private ICriteriaRefiner criteriaRefiner; private boolean useInListForPagination; /** * Constructs a new {@code QueryEntitiesAction} instance. */ public QueryEntitiesAction() { useInListForPagination = true; } /** * Create a in list criterion potentially using disjunction to overcome the * size limitation of certain DBs in restriction (e.g. Oracle is 1000). * * @param entityIds * the list of entity ids. * @param chunkSize * the size of disjunctions parts. * @return the criterion. */ public static Criterion createEntityIdsInCriterion(Collection<Serializable> entityIds, int chunkSize) { if (entityIds.size() < chunkSize) { return Restrictions.in(IEntity.ID, entityIds); } int i = 0; Disjunction splittedInlist = Restrictions.disjunction(); Set<Serializable> currentEntityIds = new LinkedHashSet<>(); boolean complete = false; for (Iterator<Serializable> ite = entityIds.iterator(); ite.hasNext(); i++) { currentEntityIds.add(ite.next()); if (i % chunkSize == (chunkSize - 1)) { splittedInlist.add(Restrictions.in(IEntity.ID, currentEntityIds)); currentEntityIds = new LinkedHashSet<>(); complete = true; } else { complete = false; } } if (!complete) { splittedInlist.add(Restrictions.in(IEntity.ID, currentEntityIds)); } return splittedInlist; } /** * Performs actual Query. This method can be overridden by subclasses in order * to deal with non-Hibernate searches. * * @param queryComponent * the query component. * @param context * the action context * @return the list of retrieved components. */ @Override @SuppressWarnings({"unchecked", "ConstantConditions"}) public List<?> performQuery(final IQueryComponent queryComponent, final Map<String, Object> context) { Session hibernateSession = ((HibernateBackendController) getController(context)).getHibernateSession(); ICriteriaFactory critFactory = (ICriteriaFactory) queryComponent.get(CRITERIA_FACTORY); if (critFactory == null) { critFactory = getCriteriaFactory(context); queryComponent.put(CRITERIA_FACTORY, critFactory); } EnhancedDetachedCriteria criteria = critFactory.createCriteria(queryComponent, context); List<IEntity> entities; if (criteria == null) { entities = new ArrayList<>(); queryComponent.setRecordCount(0); } else { ICriteriaRefiner critRefiner = (ICriteriaRefiner) queryComponent.get(CRITERIA_REFINER); if (critRefiner == null) { critRefiner = getCriteriaRefiner(context); if (critRefiner != null) { queryComponent.put(CRITERIA_REFINER, critRefiner); } } if (critRefiner != null) { critRefiner.refineCriteria(criteria, queryComponent, context); } Integer totalCount = null; Integer pageSize = queryComponent.getPageSize(); Integer page = queryComponent.getPage(); ResultTransformer refinerResultTransformer = criteria.getResultTransformer(); List<Order> refinerOrders = criteria.getOrders(); if (refinerOrders != null) { criteria.removeAllOrders(); } if (queryComponent.isDistinctEnforced() || queryComponent.getQueryDescriptor().isTranslatable()) { criteria.setProjection(Projections.distinct(Projections.id())); EnhancedDetachedCriteria outerCriteria = EnhancedDetachedCriteria.forEntityName( queryComponent.getQueryContract().getName()); outerCriteria.add(Subqueries.propertyIn(IEntity.ID, criteria)); criteria = outerCriteria; } if (pageSize != null) { if (page == null) { page = 0; queryComponent.setPage(page); } if (queryComponent.getRecordCount() == null) { if (isUseCountForPagination()) { criteria.setProjection(Projections.rowCount()); totalCount = ((Number) criteria.getExecutableCriteria(hibernateSession).list().get(0)).intValue(); } else { totalCount = IQueryComponent.UNKNOWN_COUNT; } } if (refinerOrders != null) { for (Order order : refinerOrders) { criteria.addOrder(order); } } critFactory.completeCriteriaWithOrdering(criteria, queryComponent, context); if (refinerResultTransformer != null) { criteria.setResultTransformer(refinerResultTransformer); } if (useInListForPagination) { criteria.setProjection(Projections.id()); List<Serializable> entityIds = criteria.getExecutableCriteria(hibernateSession).setFirstResult( page * pageSize).setMaxResults(pageSize).list(); if (entityIds.isEmpty()) { entities = new ArrayList<>(); } else { criteria = EnhancedDetachedCriteria.forEntityName(queryComponent.getQueryContract().getName()); entities = criteria.add(createEntityIdsInCriterion(entityIds, 500)).getExecutableCriteria(hibernateSession) .list(); Map<Serializable, IEntity> entitiesById = new HashMap<>(); for (IEntity entity : entities) { entitiesById.put(entity.getId(), entity); } entities = new ArrayList<>(); for (Serializable id : entityIds) { IEntity entity = entitiesById.get(id); if (entity != null) { entities.add(entity); } } } } else { entities = criteria.getExecutableCriteria(hibernateSession).setFirstResult(page * pageSize).setMaxResults( pageSize).list(); } } else { if (refinerOrders != null) { for (Order order : refinerOrders) { criteria.addOrder(order); } } critFactory.completeCriteriaWithOrdering(criteria, queryComponent, context); if (refinerResultTransformer != null) { criteria.setResultTransformer(refinerResultTransformer); } entities = criteria.getExecutableCriteria(hibernateSession).list(); totalCount = entities.size(); } if (totalCount != null) { queryComponent.setRecordCount(totalCount); } } List<String> prefetchProperties = queryComponent.getPrefetchProperties(); if (prefetchProperties != null && entities != null) { // Will load the prefetch properties in the same transaction in order to leverage // Hibernate batch fetching. IAccessorFactory accessorFactory = getAccessorFactory(context); for (String prefetchProperty : prefetchProperties) { try { IAccessor propertyAccessor = accessorFactory.createPropertyAccessor(prefetchProperty, queryComponent.getQueryContract()); for (IEntity entity : entities) { try { propertyAccessor.getValue(entity); } catch (Exception e) { LOG.warn("An unexpected exception occurred when pre-fetching property {}", prefetchProperty, e); } } } catch (Exception e) { LOG.warn("An unexpected exception occurred when pre-fetching property {}", prefetchProperty, e); } } } return entities; } /** * Configures a criteria refiner that will be called before the Hibernate * detached criteria is actually used to perform the query. It allows to * complement the criteria with arbitrary complex clauses that cannot be * simply expressed in a "<i>Query by Example</i>" semantics. * * @param criteriaRefiner * the criteriaRefiner to set. */ public void setCriteriaRefiner(ICriteriaRefiner criteriaRefiner) { this.criteriaRefiner = criteriaRefiner; } /** * Retrieves the configured criteria refiner. * * @param context * the action context. * @return the configured criteria refiner. */ public ICriteriaRefiner getCriteriaRefiner(Map<String, Object> context) { if (context.containsKey(CRITERIA_REFINER)) { return (ICriteriaRefiner) context.get(CRITERIA_REFINER); } return this.criteriaRefiner; } /** * Gets the criteriaFactory. * * @param context * the action context. * @return the criteriaFactory. */ protected ICriteriaFactory getCriteriaFactory(Map<String, Object> context) { if (context.containsKey(CRITERIA_FACTORY)) { return (ICriteriaFactory) context.get(CRITERIA_FACTORY); } return criteriaFactory; } /** * Sets the criteriaFactory. * * @param criteriaFactory * the criteriaFactory to set. */ public void setCriteriaFactory(ICriteriaFactory criteriaFactory) { this.criteriaFactory = criteriaFactory; } /** * Sets the useInListForPagination. * * @param useInListForPagination * the useInListForPagination to set. */ public void setUseInListForPagination(boolean useInListForPagination) { this.useInListForPagination = useInListForPagination; } }