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    }