001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.module.bc.batch.dataaccess.impl; 017 018 import java.util.HashMap; 019 import java.util.Iterator; 020 021 import org.apache.ojb.broker.metadata.ClassDescriptor; 022 import org.apache.ojb.broker.metadata.FieldDescriptor; 023 import org.apache.ojb.broker.metadata.MetadataManager; 024 import org.apache.ojb.broker.query.Criteria; 025 import org.apache.ojb.broker.query.QueryByCriteria; 026 import org.apache.ojb.broker.query.ReportQueryByCriteria; 027 import org.kuali.rice.kns.dao.impl.PlatformAwareDaoBaseOjb; 028 import org.kuali.rice.kns.util.TransactionalServiceUtils; 029 030 031 public class BudgetConstructionBatchHelperDaoOjb extends PlatformAwareDaoBaseOjb { 032 033 /** 034 * provides methods used throughout budget constuction batch to size hashmaps for efficiency 035 * for distinct and group by queries, we try to improve on the count returned by OJB's getCount. 036 * in both cases, OJB produces a COUNT(DISTINCT concat(select fields, primary key fields)), which can substantially overcount the rows returned. 037 */ 038 /* 039 * ****************************************************************************** 040 * These are utility routines used by all the units 041 * ****************************************************************************** 042 */ 043 // return the recommended length of a hash map (to avoid collisions but avoid 044 // wasting too much space) 045 //********************************************************** 046 // our technique of doing joins in Java instead of OJB is going to use a lot of 047 // memory. since memory is a finite resource, we want the garbage collector to 048 // remove things no longer in use. we could use weak hashmaps, but since many of 049 // the hashed objects in the globally scoped hashmaps are built within the scope 050 // of a method, doing so might cause them to be trashed prematurely. instead, 051 // we instantiate all the hashmaps on declaration with a length of 1 (to avoid 052 // null pointers). then, we instantiate them again on first use with a size 053 // determined by the likely number of objects * (1/.75) (see Horstman). When 054 // we are done with the hash objects, we clear them, so the underlying objects 055 // are no longer referred to by anything and are fair game for the garbage 056 // collector. 057 //*********************************************************** 058 059 060 private String ojbPlatform; 061 062 private HashMap<String,String> countDistinctLeader = null; 063 private HashMap<String,String> countDistinctTrailer = null; 064 private HashMap<String,String> concatSeparator = null; 065 066 // a MAX, say, with a GROUP BY would return one row. So, would COUNT (DISTINCT 1) 067 private Integer DEFAULT_QUERY_RETURN_COUNT = 1; 068 069 070 071 /* 072 * takes an OJB queryByCriteria object as input 073 * returns the recommended size of a hashmap that is to be created from the results of the query 074 * (the recommend size is calculated from the number of rows the query is expected to return using a formula 075 * given in the JDK5 HashMap class comments) 076 */ 077 protected Integer hashCapacity(QueryByCriteria queryID) 078 { 079 // this corresponds to a little more than the default load factor of .75. 080 // a rehash supposedly occurs when the actual number of elements exceeds (load factor)*capacity. 081 // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. 082 Double tempValue = ((Number)(getPersistenceBrokerTemplate().getCount(queryID))).floatValue()*(1.45); 083 return (Integer) tempValue.intValue(); 084 } 085 086 /* 087 * takes an OJB reportQueryByCriteria object as input 088 * this is a second version of the overloaded method hashcapacity 089 */ 090 091 protected Integer hashCapacity(ReportQueryByCriteria queryID) 092 { 093 // 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. 094 // our method will have less chance of overcounting for Oracle and MySQL. 095 // it will default for other DB's at present. 096 if (queryID.isDistinct() || (!(queryID.getGroupBy().isEmpty()))) 097 { 098 Double tempValue = queryCountDistinct(queryID).floatValue()*(1.45); 099 return (Integer) tempValue.intValue(); 100 } 101 // 102 // since the query does not contain a DISTINCT or a GROUPBY, we use OJB's getCount. 103 // this corresponds to a little more than the default load factor of .75 104 // a rehash supposedly occurs when the actual number of elements exceeds 105 // (load factor)*capacity 106 // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. 107 Double tempValue = ((Number)(getPersistenceBrokerTemplate().getCount(queryID))).floatValue()*(1.45); 108 return (Integer) tempValue.intValue(); 109 } 110 protected Integer hashCapacity(Integer hashSize) 111 { 112 // this corresponds to a little more than the default load factor of .75 113 // a rehash supposedly occurs when the actual number of elements exceeds 114 // (load factor)*capacity 115 // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N. We add a little slop. 116 Double tempValue = hashSize.floatValue()*(1.45); 117 return (Integer) tempValue.intValue(); 118 } 119 120 protected Integer hashObjectSize(Class classID, Criteria criteriaID) 121 { 122 // this counts all rows 123 String[] selectList = new String[] {"COUNT(*)"}; 124 ReportQueryByCriteria queryID = 125 new ReportQueryByCriteria(classID, selectList, criteriaID); 126 Iterator resultRows = 127 getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 128 if (resultRows.hasNext()) 129 { 130 return(hashCapacity(((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); 131 } 132 return (new Integer(1)); 133 } 134 135 protected Integer hashObjectSize(Class classID, Criteria criteriaID, 136 String propertyName) 137 { 138 // this one counts distinct values of a single field 139 String[] selectList = buildCountDistinct(propertyName,classID); 140 // if the field is not found, return the default 141 if (selectList[0] == null) 142 { 143 return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); 144 } 145 ReportQueryByCriteria queryID = 146 new ReportQueryByCriteria(classID, selectList, criteriaID); 147 Iterator resultRows = 148 getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 149 if (resultRows.hasNext()) 150 { 151 return(hashCapacity(((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); 152 } 153 return (new Integer(1)); 154 } 155 156 protected Integer hashObjectSize(Class classID, Criteria criteriaID, 157 String[] selectList) 158 { 159 // this version is designed to do a count of distinct composite key values 160 // it is assumed that the key's components can all be concatenated 161 // there is apparently no concatenation function that is supported in all 162 // versions of SQL (even though there is a standard) 163 // OJB's getCount does NOT handle this properly (it counts the number of primary keys) 164 // so, we use a helper method to build our own DISTINCT query, based on the OJB platform 165 // if the platform is not Oracle or MySQL, we return a default size 166 ReportQueryByCriteria queryID = 167 new ReportQueryByCriteria(classID, selectList, criteriaID, true); 168 return (hashCapacity(queryID)); 169 } 170 171 protected String[] buildCountDistinct(ReportQueryByCriteria originalQuery) 172 { 173 // build the select list element COUNT(DISTINCT from the input query. 174 // return an empty array for the SELECT list if this is not possible. 175 boolean fieldValueFound = false; 176 String[] returnSelectList = {""}; 177 if (! countDistinctLeader.containsKey(ojbPlatform)) 178 { 179 // the ojbPlatform is not registered in this Kuali implementation 180 return returnSelectList; 181 } 182 StringBuilder countDistinctElement = new StringBuilder(500); 183 countDistinctElement.append(countDistinctLeader.get(ojbPlatform)); 184 // now we have to find the DB column names (as opposed to the OJB attribute names) for the fields in the SELECT list 185 HashMap<String,String> allFields = getDBFieldNamesForClass(originalQuery.getSearchClass()); 186 String[] querySelectList = originalQuery.getAttributes(); 187 for (String attributeName : querySelectList) 188 { 189 String columnName = allFields.get(attributeName); 190 if (columnName != null) 191 { 192 // add a separator if there was a previous column 193 if (fieldValueFound) { countDistinctElement.append(concatSeparator.get(ojbPlatform)); } 194 // stick in the new column 195 countDistinctElement.append(columnName); 196 // indicate that one of the original select entries is a DB field name 197 fieldValueFound = true; 198 } 199 } 200 if (! fieldValueFound) 201 { 202 // none of the items in the SELECT list is a DB-field, so no COUNT(DISTINCT is possible. 203 return returnSelectList; 204 } 205 countDistinctElement.append(countDistinctTrailer.get(ojbPlatform)); 206 returnSelectList[0] = countDistinctElement.toString(); 207 return returnSelectList; 208 } 209 210 protected String[] buildCountDistinct(String ojbAttributeName, Class ojbClass) 211 { 212 String[] returnSelectList = {""}; 213 // get the attribute/DB column name map for the class 214 HashMap<String,String> allFields = getDBFieldNamesForClass(ojbClass); 215 // build a COUNT( DISTINCT wrapper around the DB column name 216 String dbColumnName = allFields.get(ojbAttributeName); 217 if (dbColumnName == null) 218 { 219 // return an empty list if we fail 220 return returnSelectList; 221 } 222 returnSelectList[0] = "COUNT(DISTINCT "+dbColumnName+")"; 223 return returnSelectList; 224 } 225 226 /** 227 * 228 * fetch the DB column names for the fields in the class for the query 229 * @param ojbClass = class of the query 230 * @return hash set of DB column names keyed by OJB attribute name, 231 */ 232 protected HashMap<String,String> getDBFieldNamesForClass(Class ojbClass) 233 { 234 ClassDescriptor ojbClassDescriptor = MetadataManager.getInstance().getRepository().getDescriptorFor(ojbClass); 235 FieldDescriptor[] fieldDescriptorArray = ojbClassDescriptor.getFieldDescriptions(); 236 HashMap<String,String> returnSet = new HashMap<String,String>(((Double)(1.34*fieldDescriptorArray.length)).intValue()); 237 for (FieldDescriptor fieldInDB: fieldDescriptorArray) 238 { 239 returnSet.put(fieldInDB.getAttributeName(),fieldInDB.getColumnName()); 240 } 241 return returnSet; 242 } 243 244 /** 245 * 246 * 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 247 * @param originalQuery = OJB report query for which to find a value for the row count returned 248 * @return: number of rows the query should return 249 */ 250 protected Integer queryCountDistinct(ReportQueryByCriteria originalQuery) 251 { 252 // for every query with a distinct attribute, or with a group by: 253 // we will build a COUNT(DISTINCT ...) with proper concatentation for Oracle and MySQL based on the fields in the select list. 254 // for other databases we will simply return a default size 255 // for queries that do not have a distinct or group by, we will simply return OJB's getCount 256 String[] countDistinctElement = buildCountDistinct(originalQuery); 257 // we return the default if there were no field names in the select list 258 if (countDistinctElement[0] == null) 259 { 260 return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); 261 } 262 Class targetClass = originalQuery.getSearchClass(); 263 Criteria criteriaID = originalQuery.getCriteria(); 264 ReportQueryByCriteria countQuery = new ReportQueryByCriteria(targetClass, countDistinctElement, criteriaID); 265 // run the new COUNT(DISTINCT query in OJB, and return the result 266 Iterator resultRows = 267 getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(countQuery); 268 if (resultRows.hasNext()) 269 { 270 return((Integer) (((Number)((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue())); 271 } 272 return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT)); 273 } 274 275 public String getOjbPlatform() 276 { 277 return this.ojbPlatform; 278 } 279 280 /** 281 * 282 * initialize the ojbPlatform from the configuration properties 283 * @param ojbPlatform = configuration property indicating the DB platform in use 284 */ 285 public void setOjbPlatform(String ojbPlatform) 286 { 287 this.ojbPlatform = ojbPlatform; 288 } 289 290 291 /** 292 * 293 * build the Oracle-specific COUNT (DISTINCT syntax--which is ANSI standard 294 * @param ojbOraclePlatform is the Kuali constant matching the configuration property for Oracle 295 */ 296 public void setOjbOraclePlatform(String ojbOraclePlatform) 297 { 298 // set up the Oracle-specific syntax in an associative array so it's easy to access 299 if (countDistinctLeader == null) 300 { 301 countDistinctLeader = new HashMap<String,String>(); 302 } 303 if (countDistinctTrailer == null) 304 { 305 countDistinctTrailer = new HashMap<String,String>(); 306 } 307 if (concatSeparator == null) 308 { 309 concatSeparator = new HashMap<String,String>(); 310 } 311 countDistinctLeader.put(ojbOraclePlatform,new String("COUNT( DISTINCT ")); 312 countDistinctTrailer.put(ojbOraclePlatform,new String(")")); 313 concatSeparator.put(ojbOraclePlatform,new String("||")); 314 } 315 316 /** 317 * 318 * build the MYSQL-specific COUNT(DISTINCT syntax 319 * @param ojbMySqlPlatform is the Kuali constant matching the configuration property for MySQL 320 */ 321 public void setOjbMySqlPlatform(String ojbMySqlPlatform) 322 { 323 // set up the MYSQL-specific syntax in an associative array so it's easy to access 324 if (countDistinctLeader == null) 325 { 326 countDistinctLeader = new HashMap<String,String>(); 327 } 328 if (countDistinctTrailer == null) 329 { 330 countDistinctTrailer = new HashMap<String,String>(); 331 } 332 if (concatSeparator == null) 333 { 334 concatSeparator = new HashMap<String,String>(); 335 } 336 countDistinctLeader.put(ojbMySqlPlatform,new String("COUNT( DISTINCT CONCAT(")); 337 countDistinctTrailer.put(ojbMySqlPlatform,new String("))")); 338 concatSeparator.put(ojbMySqlPlatform,new String(",")); 339 } 340 341 }