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 }