/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.bc.batch.dataaccess.impl; import java.util.HashMap; import java.util.Iterator; import org.apache.ojb.broker.metadata.ClassDescriptor; import org.apache.ojb.broker.metadata.FieldDescriptor; import org.apache.ojb.broker.metadata.MetadataManager; import org.apache.ojb.broker.query.Criteria; import org.apache.ojb.broker.query.QueryByCriteria; import org.apache.ojb.broker.query.ReportQueryByCriteria; import org.kuali.kfs.sys.util.TransactionalServiceUtils; import org.kuali.rice.core.framework.persistence.ojb.dao.PlatformAwareDaoBaseOjb; public class BudgetConstructionBatchHelperDaoOjb extends PlatformAwareDaoBaseOjb { /** * provides methods used throughout budget constuction batch to size hashmaps for efficiency * for distinct and group by queries, we try to improve on the count returned by OJB's getCount. * in both cases, OJB produces a COUNT(DISTINCT concat(select fields, primary key fields)), which can substantially overcount the rows returned. */ /* * ****************************************************************************** * These are utility routines used by all the units * ****************************************************************************** */ // return the recommended length of a hash map (to avoid collisions but avoid // wasting too much space) //********************************************************** // our technique of doing joins in Java instead of OJB is going to use a lot of // memory. since memory is a finite resource, we want the garbage collector to // remove things no longer in use. we could use weak hashmaps, but since many of // the hashed objects in the globally scoped hashmaps are built within the scope // of a method, doing so might cause them to be trashed prematurely. instead, // we instantiate all the hashmaps on declaration with a length of 1 (to avoid // null pointers). then, we instantiate them again on first use with a size // determined by the likely number of objects * (1/.75) (see Horstman). When // we are done with the hash objects, we clear them, so the underlying objects // are no longer referred to by anything and are fair game for the garbage // collector. //*********************************************************** private String ojbPlatform; private HashMap<String,String> countDistinctLeader = null; private HashMap<String,String> countDistinctTrailer = null; private HashMap<String,String> concatSeparator = null; // a MAX, say, with a GROUP BY would return one row. So, would COUNT (DISTINCT 1) private Integer DEFAULT_QUERY_RETURN_COUNT = 1; /* * takes an OJB queryByCriteria object as input * returns the recommended size of a hashmap that is to be created from the results of the query * (the recommend size is calculated from the number of rows the query is expected to return using a formula * given in the JDK5 HashMap class comments) */ protected Integer hashCapacity(QueryByCriteria queryID) { // this corresponds to a little more than the default load factor of .75. // a rehash supposedly occurs when the actual number of elements exceeds (load factor)*capacity. // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. Double tempValue = ((Number)(getPersistenceBrokerTemplate().getCount(queryID))).floatValue()*(1.45); return (Integer) tempValue.intValue(); } /* * takes an OJB reportQueryByCriteria object as input * this is a second version of the overloaded method hashcapacity */ protected Integer hashCapacity(ReportQueryByCriteria queryID) { // for a distinct or a group by query, we build our own COUNT(DISTINCT...) from the fields in the SELECT list, because OJB doesn't do this correctly. // our method will have less chance of overcounting for Oracle and MySQL. // it will default for other DB's at present. if (queryID.isDistinct() || (!(queryID.getGroupBy().isEmpty()))) { Double tempValue = queryCountDistinct(queryID).floatValue()*(1.45); return (Integer) tempValue.intValue(); } // // since the query does not contain a DISTINCT or a GROUPBY, we use OJB's getCount. // this corresponds to a little more than the default load factor of .75 // a rehash supposedly occurs when the actual number of elements exceeds // (load factor)*capacity // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. Double tempValue = ((Number)(getPersistenceBrokerTemplate().getCount(queryID))).floatValue()*(1.45); return (Integer) tempValue.intValue(); } protected Integer hashCapacity(Integer hashSize) { // this corresponds to a little more than the default load factor of .75 // a rehash supposedly occurs when the actual number of elements exceeds // (load factor)*capacity // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. Double tempValue = hashSize.floatValue()*(1.45); return (Integer) tempValue.intValue(); } protected Integer hashObjectSize(Class classID, Criteria criteriaID) { // this counts all rows String[] selectList = new String[] {"COUNT(*)"}; ReportQueryByCriteria queryID = new ReportQueryByCriteria(classID, selectList, criteriaID); Iterator resultRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); if (resultRows.hasNext()) { return(hashCapacity(((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); } return (new Integer(1)); } protected Integer hashObjectSize(Class classID, Criteria criteriaID, String propertyName) { // this one counts distinct values of a single field String[] selectList = buildCountDistinct(propertyName,classID); // if the field is not found, return the default if (selectList[0] == null) { return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); } ReportQueryByCriteria queryID = new ReportQueryByCriteria(classID, selectList, criteriaID); Iterator resultRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); if (resultRows.hasNext()) { return(hashCapacity(((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); } return (new Integer(1)); } protected Integer hashObjectSize(Class classID, Criteria criteriaID, String[] selectList) { // this version is designed to do a count of distinct composite key values // it is assumed that the key's components can all be concatenated // there is apparently no concatenation function that is supported in all // versions of SQL (even though there is a standard) // OJB's getCount does NOT handle this properly (it counts the number of primary keys) // so, we use a helper method to build our own DISTINCT query, based on the OJB platform // if the platform is not Oracle or MySQL, we return a default size ReportQueryByCriteria queryID = new ReportQueryByCriteria(classID, selectList, criteriaID, true); return (hashCapacity(queryID)); } protected String[] buildCountDistinct(ReportQueryByCriteria originalQuery) { // build the select list element COUNT(DISTINCT from the input query. // return an empty array for the SELECT list if this is not possible. boolean fieldValueFound = false; String[] returnSelectList = {""}; if (! countDistinctLeader.containsKey(ojbPlatform)) { // the ojbPlatform is not registered in this Kuali implementation return returnSelectList; } StringBuilder countDistinctElement = new StringBuilder(500); countDistinctElement.append(countDistinctLeader.get(ojbPlatform)); // now we have to find the DB column names (as opposed to the OJB attribute names) for the fields in the SELECT list HashMap<String,String> allFields = getDBFieldNamesForClass(originalQuery.getSearchClass()); String[] querySelectList = originalQuery.getAttributes(); for (String attributeName : querySelectList) { String columnName = allFields.get(attributeName); if (columnName != null) { // add a separator if there was a previous column if (fieldValueFound) { countDistinctElement.append(concatSeparator.get(ojbPlatform)); } // stick in the new column countDistinctElement.append(columnName); // indicate that one of the original select entries is a DB field name fieldValueFound = true; } } if (! fieldValueFound) { // none of the items in the SELECT list is a DB-field, so no COUNT(DISTINCT is possible. return returnSelectList; } countDistinctElement.append(countDistinctTrailer.get(ojbPlatform)); returnSelectList[0] = countDistinctElement.toString(); return returnSelectList; } protected String[] buildCountDistinct(String ojbAttributeName, Class ojbClass) { String[] returnSelectList = {""}; // get the attribute/DB column name map for the class HashMap<String,String> allFields = getDBFieldNamesForClass(ojbClass); // build a COUNT( DISTINCT wrapper around the DB column name String dbColumnName = allFields.get(ojbAttributeName); if (dbColumnName == null) { // return an empty list if we fail return returnSelectList; } returnSelectList[0] = "COUNT(DISTINCT "+dbColumnName+")"; return returnSelectList; } /** * * fetch the DB column names for the fields in the class for the query * @param ojbClass = class of the query * @return hash set of DB column names keyed by OJB attribute name, */ protected HashMap<String,String> getDBFieldNamesForClass(Class ojbClass) { ClassDescriptor ojbClassDescriptor = MetadataManager.getInstance().getRepository().getDescriptorFor(ojbClass); FieldDescriptor[] fieldDescriptorArray = ojbClassDescriptor.getFieldDescriptions(); HashMap<String,String> returnSet = new HashMap<String,String>(((Double)(1.34*fieldDescriptorArray.length)).intValue()); for (FieldDescriptor fieldInDB: fieldDescriptorArray) { returnSet.put(fieldInDB.getAttributeName(),fieldInDB.getColumnName()); } return returnSet; } /** * * build a correct, DB-specific COUNT DISTINCT query to indicate how many rows a distinct or GROUP BY query will return. the default count is returned if this is not possible * @param originalQuery = OJB report query for which to find a value for the row count returned * @return: number of rows the query should return */ protected Integer queryCountDistinct(ReportQueryByCriteria originalQuery) { // for every query with a distinct attribute, or with a group by: // we will build a COUNT(DISTINCT ...) with proper concatentation for Oracle and MySQL based on the fields in the select list. // for other databases we will simply return a default size // for queries that do not have a distinct or group by, we will simply return OJB's getCount String[] countDistinctElement = buildCountDistinct(originalQuery); // we return the default if there were no field names in the select list if (countDistinctElement[0] == null) { return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); } Class targetClass = originalQuery.getSearchClass(); Criteria criteriaID = originalQuery.getCriteria(); ReportQueryByCriteria countQuery = new ReportQueryByCriteria(targetClass, countDistinctElement, criteriaID); // run the new COUNT(DISTINCT query in OJB, and return the result Iterator resultRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(countQuery); if (resultRows.hasNext()) { return((Integer) (((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); } return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); } public String getOjbPlatform() { return this.ojbPlatform; } /** * * initialize the ojbPlatform from the configuration properties * @param ojbPlatform = configuration property indicating the DB platform in use */ public void setOjbPlatform(String ojbPlatform) { this.ojbPlatform = ojbPlatform; } /** * * build the Oracle-specific COUNT (DISTINCT syntax--which is ANSI standard * @param ojbOraclePlatform is the Kuali constant matching the configuration property for Oracle */ public void setOjbOraclePlatform(String ojbOraclePlatform) { // set up the Oracle-specific syntax in an associative array so it's easy to access if (countDistinctLeader == null) { countDistinctLeader = new HashMap<String,String>(); } if (countDistinctTrailer == null) { countDistinctTrailer = new HashMap<String,String>(); } if (concatSeparator == null) { concatSeparator = new HashMap<String,String>(); } countDistinctLeader.put(ojbOraclePlatform,new String("COUNT( DISTINCT ")); countDistinctTrailer.put(ojbOraclePlatform,new String(")")); concatSeparator.put(ojbOraclePlatform,new String("||")); } /** * * build the MYSQL-specific COUNT(DISTINCT syntax * @param ojbMySqlPlatform is the Kuali constant matching the configuration property for MySQL */ public void setOjbMySqlPlatform(String ojbMySqlPlatform) { // set up the MYSQL-specific syntax in an associative array so it's easy to access if (countDistinctLeader == null) { countDistinctLeader = new HashMap<String,String>(); } if (countDistinctTrailer == null) { countDistinctTrailer = new HashMap<String,String>(); } if (concatSeparator == null) { concatSeparator = new HashMap<String,String>(); } countDistinctLeader.put(ojbMySqlPlatform,new String("COUNT( DISTINCT CONCAT(")); countDistinctTrailer.put(ojbMySqlPlatform,new String("))")); concatSeparator.put(ojbMySqlPlatform,new String(",")); } }