/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.domain.server; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.management.InstanceAlreadyExistsException; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; import javax.management.ObjectName; import javax.persistence.EntityManager; import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.ejb.EntityManagerImpl; import org.hibernate.engine.spi.NamedQueryDefinition; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.jmx.StatisticsService; import org.hibernate.stat.Statistics; import org.hibernate.type.CustomType; import org.hibernate.type.EntityType; import org.hibernate.type.PrimitiveType; import org.hibernate.type.Type; import org.rhq.core.domain.util.OrderingField; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.core.domain.util.PageOrdering; /** * Various persistence utility methods - mostly Hibernate-specific. * * @author Heiko Rupp * @author Joseph Marques * @author Greg Hinkle */ public class PersistenceUtility { private static final Log LOG = LogFactory.getLog(PersistenceUtility.class); private static final Pattern COUNT_QUERY_PATTERN = Pattern.compile("^(\\s*SELECT\\s+)(.*?)(\\s+FROM.*)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); private static final Pattern COUNT_QUERY_REMOVE_FETCH = Pattern.compile("FETCH", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); public static final String HIBERNATE_STATISTICS_MBEAN_OBJECTNAME = "Hibernate:type=statistics,application=RHQ"; @SuppressWarnings("unchecked") // used in hibernate.jsp public static String getDisplayString(Type hibernateType) { if (hibernateType instanceof EntityType) { return hibernateType.getName() + " (enter integer of ID / primary key field)"; } else if (hibernateType instanceof CustomType) { if (Enum.class.isAssignableFrom(hibernateType.getReturnedClass())) { Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) hibernateType.getReturnedClass(); StringBuilder result = new StringBuilder(); result.append(enumClass.getName()); result.append(" ("); boolean first = true; for (Enum<?> nextEnum : enumClass.getEnumConstants()) { if (!first) { result.append(" | "); } else { first = false; } result.append(nextEnum.name()); } result.append(")"); return result.toString(); } } return hibernateType == null ? "" : hibernateType.getName(); } @SuppressWarnings("unchecked") // used in hibernate.jsp public static Object cast(String value, Type hibernateType) { if (hibernateType instanceof PrimitiveType) { Class<?> type = ((PrimitiveType) hibernateType).getPrimitiveClass(); if (type.equals(Byte.TYPE)) { return Byte.valueOf(value); } else if (type.equals(Short.TYPE)) { return Short.valueOf(value); } else if (type.equals(Integer.TYPE)) { return Integer.valueOf(value); } else if (type.equals(Long.TYPE)) { return Long.valueOf(value); } else if (type.equals(Float.TYPE)) { return Float.valueOf(value); } else if (type.equals(Double.TYPE)) { return Double.valueOf(value); } else if (type.equals(Boolean.TYPE)) { return Boolean.valueOf(value); } } else if (hibernateType instanceof EntityType) { String entityName = ((EntityType) hibernateType).getAssociatedEntityName(); try { Class<?> entityClass = Class.forName(entityName); Object entity = entityClass.newInstance(); Field primaryKeyField = entityClass.getDeclaredField("id"); primaryKeyField.setAccessible(true); primaryKeyField.setInt(entity, Integer.valueOf(value)); return entity; } catch (Throwable t) { throw new IllegalArgumentException("Type[" + entityName + "] must have PK field named 'id'"); } } else if (hibernateType instanceof CustomType) { if (Enum.class.isAssignableFrom(hibernateType.getReturnedClass())) { Class<? extends Enum<?>> enumClass = hibernateType.getReturnedClass(); Enum<?>[] enumValues = enumClass.getEnumConstants(); try { int enumOrdinal = Integer.valueOf(value); try { return enumValues[enumOrdinal]; } catch (ArrayIndexOutOfBoundsException aioobe) { throw new IllegalArgumentException("There is no " + enumClass.getSimpleName() + " enum with ordinal '" + enumOrdinal + "'"); } } catch (NumberFormatException nfe) { String ucaseValue = value.toUpperCase(); for (Enum<?> nextEnum : enumValues) { if (nextEnum.name().toUpperCase().equals(ucaseValue)) { return nextEnum; } } throw new IllegalArgumentException("There is no " + enumClass.getSimpleName() + " enum with name '" + value + "'"); } } } return value; } /** * Used to create queries to use with the {@link org.rhq.core.domain.util.PageControl} objects. The query will already have its sort column * and order appended as well as having its first result and max results set according to the page control data. * * @param entityManager your entity manager * @param queryName name of the query * @param pageControl the controls on the paging and sorting * * @return a preconfigured query for ordered pagination */ public static Query createQueryWithOrderBy(EntityManager entityManager, String queryName, PageControl pageControl) { Query query; if (pageControl.getPrimarySortColumn() != null) { query = createQueryWithOrderBy(entityManager, queryName, pageControl.getOrderingFieldsAsArray()); } else { StackTraceElement caller = new Throwable().fillInStackTrace().getStackTrace()[1]; LOG.warn("Queries should really supply default sort columns. Caller did not: " + caller); // Use the standard named query if no sorting is specified query = entityManager.createNamedQuery(queryName); } setDataPage(query, pageControl); return query; } /** * Create a query from a named query with a transformed order by clause with multiple new ordery by clauses. * * @param entityManager the entity manager to build the query against * @param queryName the name of the query to transform * @param orderByFields an array of clauses to contribute to the order by * * @return the transformed query */ public static Query createQueryWithOrderBy(EntityManager entityManager, String queryName, OrderingField... orderByFields) { NamedQueryDefinition ndc = getNamedQueryDefinition(entityManager, queryName); StringBuilder query = new StringBuilder(ndc.getQueryString()); buildOrderBy(query, orderByFields); return entityManager.createQuery(query.toString()); } private static StringBuilder buildOrderBy(StringBuilder query, OrderingField... orderByFields) { boolean first = true; for (OrderingField orderingField : orderByFields) { if (first) { // TODO GH: We could see if there already is an order by clause and contribute or override it query.append(" ORDER BY "); first = false; } else { query.append(", "); } query.append(orderingField.getField()).append(" ").append(orderingField.getOrdering()); } return query; } private static String getOrderByFragment(OrderingField... orderByFields) { boolean first = true; StringBuilder builder = new StringBuilder(); for (OrderingField orderingField : orderByFields) { if (first) { builder.append(" ORDER BY "); first = false; } else { builder.append(", "); } builder.append(orderingField.getField()).append(" ").append(orderingField.getOrdering()); } return builder.toString(); } /** * Builds a count(*) version of the named query so we don't have duplicate all our queries to use two query * pagination model. * * @param em the entity manager to build the query for * @param queryName the NamedQuery to transform * * @return a query that can be bound and executed to get the total count of results */ public static Query createCountQuery(EntityManager em, String queryName) { return createCountQuery(em, queryName, "*"); } /** * Builds a count(*) version of the named query so we don't have duplicate all our queries to use two query * pagination model. * * @param entityManager the entity manager to build the query for * @param queryName the NamedQuery to transform * @param countItem the object or attribute that needs to be counted, when it's ambiguous * * @return a query that can be bound and executed to get the total count of results */ public static Query createCountQuery(EntityManager entityManager, String queryName, String countItem) { NamedQueryDefinition namedQueryDefinition = getNamedQueryDefinition(entityManager, queryName); String query = namedQueryDefinition.getQueryString(); Matcher matcher = COUNT_QUERY_PATTERN.matcher(query); if (!matcher.find()) { throw new RuntimeException("Unable to transform query into count query [" + queryName + " - " + query + "]"); } String newQuery = matcher.group(1) + "COUNT(" + countItem + ")" + matcher.group(3); matcher = COUNT_QUERY_REMOVE_FETCH.matcher(newQuery); if (matcher.find()) { StringBuffer buffer = new StringBuffer(); do { matcher.appendReplacement(buffer, ""); } while (matcher.find()); matcher.appendTail(buffer); newQuery = buffer.toString(); } if (LOG.isTraceEnabled()) { LOG.trace("Transformed query to count query [" + queryName + "] resulting in [" + newQuery + "]"); } return entityManager.createQuery(newQuery); } public static void setDataPage(Query query, PageControl pageControl) { if (pageControl.getPageSize() > 0) { query.setFirstResult(pageControl.getStartRow()); query.setMaxResults(pageControl.getPageSize()); } } /** * Creates and executes a filter query for a collection relationship. This executes without passing back the query * object because the most common case is to simply paginate for a relationship. Use the createFilter method to * create more generic filters and get access to the hibernate query object for setting parameters etc. * * @param entityManager * @param collection * @param pageControl * * @return the result list of the entities from the filtered relationship */ @SuppressWarnings("unchecked") public static PageList createPaginationFilter(EntityManager entityManager, Collection collection, PageControl pageControl) { if (collection == null) { return new PageList(pageControl); } String filter = ""; if (pageControl.getPrimarySortColumn() != null) { PageOrdering order = (pageControl.getPrimarySortOrder() == null) ? PageOrdering.ASC : pageControl .getPrimarySortOrder(); filter = getOrderByFragment(new OrderingField(pageControl.getPrimarySortColumn(), order)); } org.hibernate.Query query = getHibernateSession(entityManager).createFilter(collection, filter); if (pageControl.getPageSize() > 0) { query.setFirstResult(pageControl.getPageNumber() * pageControl.getPageSize()); query.setMaxResults(pageControl.getPageSize()); } // TODO GH: Always flushing is probably not what we really want here // relationship filters don't seem to cause the proper flush, so manually flush getHibernateSession(entityManager).flush(); // TODO GH: This can only create unbounded PageLists since I don't know how to do a count query to find the size return new PageList<Object>(query.list(), pageControl); } /** * Use this inside subclasses as a convenience method. */ @SuppressWarnings("unchecked") public static <T> List<T> findByCriteria(EntityManager entityManager, Class<T> type, org.hibernate.criterion.Criterion... criterion) { // Using Hibernate, more difficult with EntityManager and EJB-QL org.hibernate.Criteria crit = getHibernateSession(entityManager).createCriteria(type); for (org.hibernate.criterion.Criterion c : criterion) { crit.add(c); } return crit.list(); } public static Session getHibernateSession(EntityManager entityManager) { Session session; if (entityManager.getDelegate() instanceof EntityManagerImpl) { EntityManagerImpl entityManagerImpl = (EntityManagerImpl) entityManager.getDelegate(); session = entityManagerImpl.getSession(); } else { session = (Session) entityManager.getDelegate(); } return session; } /** * Enables the hibernate statistics mbean to provide access to information on the ejb3 persistence tier. * * @param entityManager an inject entity manager whose session factory will be tracked with these statistics * @param server the MBeanServer where the statistics MBean should be registered; if <code>null</code>, the * first one in the list returned by MBeanServerFactory.findMBeanServer(null) is used */ public static void enableHibernateStatistics(EntityManager entityManager, MBeanServer server) { enableHibernateStatistics(entityManager, server, true); } /** * Enables the hibernate statistics mbean to provide access to information on the ejb3 persistence tier. * * @param entityManager an inject entity manager whose session factory will be tracked with these statistics * @param server the MBeanServer where the statistics MBean should be registered; if <code>null</code>, the * first one in the list returned by MBeanServerFactory.findMBeanServer(null) is used * @param flag true if the stats are to be enabled; false to disable the stats */ public static void enableHibernateStatistics(EntityManager entityManager, MBeanServer server, boolean flag) { SessionFactory sessionFactory = null; try { sessionFactory = PersistenceUtility.getHibernateSession(entityManager).getSessionFactory(); if (server == null) { ArrayList<MBeanServer> list = MBeanServerFactory.findMBeanServer(null); server = list.get(0); } ObjectName objectName = new ObjectName(HIBERNATE_STATISTICS_MBEAN_OBJECTNAME); StatisticsService mBean = new StatisticsService(); mBean.setSessionFactory(sessionFactory); server.registerMBean(mBean, objectName); } catch (InstanceAlreadyExistsException iaee) { LOG.info("Duplicate mbean registration ignored: " + HIBERNATE_STATISTICS_MBEAN_OBJECTNAME); } catch (Exception e) { LOG.warn("Couldn't register hibernate statistics mbean", e); } try { if (sessionFactory != null) { sessionFactory.getStatistics().setStatisticsEnabled(flag); LOG.info("Hibernate statistics enable flag set to [" + flag + "]"); } } catch (Exception e) { LOG.warn("Couldn't set the statistics enable flag to [" + flag + "]", e); } } public static Statistics getStatisticsService(EntityManager entityManager, MBeanServer server) { Session hibernateSession = PersistenceUtility.getHibernateSession(entityManager); SessionFactory hibernateSessionFactory = hibernateSession.getSessionFactory(); Statistics hibernateStatistics = hibernateSessionFactory.getStatistics(); return hibernateStatistics; } private static NamedQueryDefinition getNamedQueryDefinition(EntityManager entityManager, String queryName) { SessionFactoryImplementor sessionFactory = getHibernateSessionFactoryImplementor(entityManager); NamedQueryDefinition namedQueryDefinition = sessionFactory.getNamedQuery(queryName); if (namedQueryDefinition == null) { throw new RuntimeException("EJB3 query not found [" + queryName + "]"); } return namedQueryDefinition; } private static SessionFactoryImplementor getHibernateSessionFactoryImplementor(EntityManager entityManager) { Session session = getHibernateSession(entityManager); SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) session.getSessionFactory(); return sessionFactory; } // wanted to combine postgres and oracle methods, but org.rhq.core.db.DatabaseType objects are not visible to domain public static String addPostgresNativePagingSortingToQuery(String query, PageControl pageControl) { return addLimitOffsetToQuery(query, pageControl); } // wanted to combine postgres and oracle methods, but org.rhq.core.db.DatabaseType objects are not visible to domain public static String addOracleNativePagingSortingToQuery(String query, PageControl pageControl) { StringBuilder queryWithPagingSorting = new StringBuilder(query.length() + 50); int minRowNum = pageControl.getStartRow() + 1; int maxRowNum = minRowNum + pageControl.getPageSize() - 1; // pagination calculations based off of double-projection of the results queryWithPagingSorting.append("SELECT outerResults.* FROM ( "); queryWithPagingSorting.append("SELECT innerResults.*, ROWNUM rnum FROM ( "); queryWithPagingSorting.append(query); // for oracle, order by occurs at the end of the original query, whether grouped or not queryWithPagingSorting.append(getOrderByFragment(pageControl.getOrderingFieldsAsArray())); queryWithPagingSorting.append(" ) innerResults "); // for oracle, paginate high off of the inner projection queryWithPagingSorting.append(" WHERE ROWNUM <= ").append(maxRowNum); // for oracle, paginate low off of the outer projection queryWithPagingSorting.append(" ) outerResults "); queryWithPagingSorting.append(" WHERE rnum >= ").append(minRowNum); return queryWithPagingSorting.toString(); } /** * Note: always put the rownum column at the END of the columns, so that code * which relies on index-based access to the result set data doesn't break * * Method 1: * * SELECT outerResults.* FROM ( * SELECT innerResults.*, * ROW_NUMBER() OVER( {orderByClause} ) AS rownum * FROM ( {queryWithoutOrderBy} ) AS innerResults * ) AS outerResults * WHERE rownum <= maxRowNum AND rownum >= minRowNum * * The above method fails in circumstances where the orderByClause is built up with * aliases that aren't in the explicit select list returned from queryWithoutOrderBy * * * Method 2: * * Fix above shortcomings by pushing the orderByClause into the actual select list * * SELECT singleResults.* FROM ( * {queryWithoutOrderBySelectList} * , ROW_NUMBER() OVER( {orderByClause} ) AS rownum * {queryWithoutOrderByRestOfQuery} * ) AS singleResults * WHERE rownum <= maxRowNum AND rownum >= minRowNum * * * Actually, both of the above methods have small flaws. The first can not sort by columns that * aren't in the explicit return list. The second can not sort by computed columns and subqueries * in the select list. The only way I see how this can work is by modifying the queryWithoutOrderBy * to explicitly return all parameters that will be sorted on (even if the use case wouldn't normally * require them to be in the select list), alias them, and order by the aliases by modifying the web * ui code to use those tokens when generating the sortable column headers (jmarques - June/2009) */ public static String addSQLServerNativePagingSortingToQuery(String query, PageControl pageControl) { return addSQLServerNativePagingSortingToQuery(query, pageControl, false); } public static String addSQLServerNativePagingSortingToQuery(String query, PageControl pageControl, boolean alternatePagingStyle) { StringBuilder queryWithPagingSorting = new StringBuilder(query.length() + 50); int minRowNum = pageControl.getStartRow() + 1; int maxRowNum = minRowNum + pageControl.getPageSize() - 1; String orderByClause = getOrderByFragment(pageControl.getOrderingFieldsAsArray()); if (alternatePagingStyle) { int index = findSelectListEndIndex(query); String selectList = query.substring(0, index); String restOfQuery = query.substring(index); queryWithPagingSorting.append("SELECT singleResults.* FROM ( "); queryWithPagingSorting.append(selectList); queryWithPagingSorting.append(", ROW_NUMBER() OVER( " + orderByClause + " ) AS rownum "); queryWithPagingSorting.append(restOfQuery); queryWithPagingSorting.append(") AS singleResults "); } else { queryWithPagingSorting.append("SELECT outerResults.* FROM ( "); queryWithPagingSorting.append(" SELECT innerResults.*, "); queryWithPagingSorting.append(" ROW_NUMBER() OVER( " + orderByClause + " ) AS rownum "); queryWithPagingSorting.append(" FROM ( " + query + " ) AS innerResults "); queryWithPagingSorting.append(" ) AS outerResults "); } queryWithPagingSorting.append("WHERE rownum <= ").append(maxRowNum); queryWithPagingSorting.append(" AND rownum >= ").append(minRowNum); return queryWithPagingSorting.toString(); } // beginning of from clause (not counting retrievals via subqueries) should indicate end of select list private static int findSelectListEndIndex(String query) { int nesting = 0; query = query.toLowerCase(); StringBuilder wordBuffer = new StringBuilder(); for (int i = 0; i < query.length(); i++) { char next = query.charAt(i); if (next == '(') { nesting++; } else if (next == ')') { nesting--; } else { if (nesting != 0) { continue; } if (Character.isLetter(next)) { wordBuffer.append(next); if (wordBuffer.toString().equals("from")) { return i - 4; // return index representing the character just before "from" } } else { wordBuffer.setLength(0); // clear buffer if we find any non-letter } } } throw new IllegalArgumentException("Could not find select list end index"); } // wanted to combine postgres and oracle methods, but org.rhq.core.db.DatabaseType objects are not visible to domain public static String addH2NativePagingSortingToQuery(String query, PageControl pageControl) { return addLimitOffsetToQuery(query, pageControl); } private static String addLimitOffsetToQuery(String query, PageControl pageControl) { StringBuilder queryWithPagingSorting = new StringBuilder(query.length() + 50); queryWithPagingSorting.append(query); // for postgres, first order by queryWithPagingSorting.append(getOrderByFragment(pageControl.getOrderingFieldsAsArray())); // for postgres, then paginate queryWithPagingSorting.append(" LIMIT ").append(pageControl.getPageSize()); queryWithPagingSorting.append(" OFFSET ").append(pageControl.getStartRow()); return queryWithPagingSorting.toString(); } }