001    /**
002     * Copyright 2005-2012 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.rice.krad.lookup;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.ojb.broker.query.Criteria;
020    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
021    import org.kuali.rice.core.api.CoreApiServiceLocator;
022    import org.kuali.rice.core.api.encryption.EncryptionService;
023    import org.kuali.rice.core.api.search.SearchOperator;
024    import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
025    import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
026    import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
027    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
028    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
029    import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
030    import org.kuali.rice.krad.util.KRADConstants;
031    import org.kuali.rice.krad.util.KRADPropertyConstants;
032    import org.kuali.rice.krad.util.ObjectUtils;
033    
034    import java.sql.Date;
035    import java.sql.Timestamp;
036    import java.text.ParseException;
037    import java.util.ArrayList;
038    import java.util.Calendar;
039    import java.util.HashMap;
040    import java.util.HashSet;
041    import java.util.List;
042    import java.util.Map;
043    import java.util.Set;
044    
045    /**
046     * Provides static utility methods for use within the lookup framework
047     *
048     * @author Kuali Rice Team (rice.collab@kuali.org)
049     */
050    public class LookupUtils {
051    
052        /**
053         * Uses the DataDictionary to determine whether to force uppercase the value, and if it should, then it does the
054         * uppercase, and returns the upper-cased value.
055         *
056         * @param dataObjectClass Parent DO class that the fieldName is a member of.
057         * @param fieldName Name of the field to be forced to uppercase.
058         * @param fieldValue Value of the field that may be uppercased.
059         * @return The correctly uppercased fieldValue if it should be uppercased, otherwise fieldValue is returned
060         *         unchanged.
061         */
062        public static String forceUppercase(Class<?> dataObjectClass, String fieldName, String fieldValue) {
063            // short-circuit to exit if there isnt enough information to do the forceUppercase
064            if (StringUtils.isBlank(fieldValue)) {
065                return fieldValue;
066            }
067    
068            // parameter validation
069            if (dataObjectClass == null) {
070                throw new IllegalArgumentException("Parameter dataObjectClass passed in with null value.");
071            }
072    
073            if (StringUtils.isBlank(fieldName)) {
074                throw new IllegalArgumentException("Parameter fieldName passed in with empty value.");
075            }
076    
077            if (!KRADServiceLocatorWeb.getDataDictionaryService().isAttributeDefined(dataObjectClass, fieldName)
078                    .booleanValue()) {
079                return fieldValue;
080            }
081    
082            boolean forceUpperCase = false;
083            try {
084                forceUpperCase = KRADServiceLocatorWeb.getDataDictionaryService()
085                        .getAttributeForceUppercase(dataObjectClass, fieldName).booleanValue();
086            } catch (UnknownBusinessClassAttributeException ubae) {
087                // do nothing, don't alter the fieldValue
088            }
089            if (forceUpperCase && !fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
090                return fieldValue.toUpperCase();
091            }
092    
093            return fieldValue;
094        }
095    
096        /**
097         * Uses the DataDictionary to determine whether to force uppercase the values, and if it should, then it does the
098         * uppercase, and returns the upper-cased Map of fieldname/fieldValue pairs.
099         *
100         * @param dataObjectClass Parent DO class that the fieldName is a member of.
101         * @param fieldValues A Map<String,String> where the key is the fieldName and the value is the fieldValue.
102         * @return The same Map is returned, with the appropriate values uppercased (if any).
103         */
104        public static Map<String, String> forceUppercase(Class<?> dataObjectClass, Map<String, String> fieldValues) {
105            if (dataObjectClass == null) {
106                throw new IllegalArgumentException("Parameter boClass passed in with null value.");
107            }
108    
109            if (fieldValues == null) {
110                throw new IllegalArgumentException("Parameter fieldValues passed in with null value.");
111            }
112    
113            for (String fieldName : fieldValues.keySet()) {
114                fieldValues.put(fieldName, forceUppercase(dataObjectClass, fieldName, (String) fieldValues.get(fieldName)));
115            }
116    
117            return fieldValues;
118        }
119    
120        /**
121         * Parses and returns the lookup result set limit, checking first for the limit
122         * for the class being looked up, and then the global application limit if there isn't a limit
123         * specific to this data object class
124         *
125         * @param dataObjectClass - class to get limit for
126         */
127        public static Integer getSearchResultsLimit(Class dataObjectClass) {
128            Integer limit = KRADServiceLocatorWeb.getViewDictionaryService().getResultSetLimitForLookup(dataObjectClass);
129            if (limit == null) {
130                limit = getApplicationSearchResultsLimit();
131            }
132    
133            return limit;
134        }
135    
136        /**
137         * Retrieves the default application search limit configured through
138         * a system parameter
139         */
140        public static Integer getApplicationSearchResultsLimit() {
141            String limitString = CoreFrameworkServiceLocator.getParameterService()
142                    .getParameterValueAsString(KRADConstants.KRAD_NAMESPACE,
143                            KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE,
144                            KRADConstants.SystemGroupParameterNames.LOOKUP_RESULTS_LIMIT);
145            if (limitString != null) {
146                return Integer.valueOf(limitString);
147            }
148    
149            return null;
150        }
151    
152        /**
153         * This method applies the search results limit to the search criteria for this BO
154         *
155         * @param businessObjectClass BO class to search on / get limit for
156         * @param criteria search criteria
157         * @param platform database platform
158         */
159        public static void applySearchResultsLimit(Class businessObjectClass, Criteria criteria,
160                DatabasePlatform platform) {
161            Integer limit = getSearchResultsLimit(businessObjectClass);
162            if (limit != null) {
163                platform.applyLimit(limit, criteria);
164            }
165        }
166    
167        /**
168         * Applies the search results limit to the search criteria for this BO (JPA)
169         *
170         * @param businessObjectClass BO class to search on / get limit for
171         * @param criteria search criteria
172         */
173        public static void applySearchResultsLimit(Class businessObjectClass,
174                org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria criteria) {
175            Integer limit = getSearchResultsLimit(businessObjectClass);
176            if (limit != null) {
177                criteria.setSearchLimit(limit);
178            }
179        }
180    
181        /**
182         * Determines what Timestamp should be used for active queries on effective dated records. Determination made as
183         * follows:
184         *
185         * <ul>
186         * <li>Use activeAsOfDate value from search values Map if value is not empty</li>
187         * <li>If search value given, try to convert to sql date, if conversion fails, try to convert to Timestamp</li>
188         * <li>If search value empty, use current Date</li>
189         * <li>If Timestamp value not given, create Timestamp from given Date setting the time as 1 second before midnight
190         * </ul>
191         *
192         * @param searchValues - Map containing search key/value pairs
193         * @return Timestamp to be used for active criteria
194         */
195        public static Timestamp getActiveDateTimestampForCriteria(Map searchValues) {
196            Date activeDate = CoreApiServiceLocator.getDateTimeService().getCurrentSqlDate();
197            Timestamp activeTimestamp = null;
198            if (searchValues.containsKey(KRADPropertyConstants.ACTIVE_AS_OF_DATE)) {
199                String activeAsOfDate = (String) searchValues.get(KRADPropertyConstants.ACTIVE_AS_OF_DATE);
200                if (StringUtils.isNotBlank(activeAsOfDate)) {
201                    try {
202                        activeDate = CoreApiServiceLocator.getDateTimeService()
203                                .convertToSqlDate(ObjectUtils.clean(activeAsOfDate));
204                    } catch (ParseException e) {
205                        // try to parse as timestamp
206                        try {
207                            activeTimestamp = CoreApiServiceLocator.getDateTimeService()
208                                    .convertToSqlTimestamp(ObjectUtils.clean(activeAsOfDate));
209                        } catch (ParseException e1) {
210                            throw new RuntimeException("Unable to convert date: " + ObjectUtils.clean(activeAsOfDate));
211                        }
212                    }
213                }
214            }
215    
216            // if timestamp not given set to 1 second before midnight on the given date
217            if (activeTimestamp == null) {
218                Calendar cal = Calendar.getInstance();
219                cal.setTime(activeDate);
220                cal.set(Calendar.HOUR, cal.getMaximum(Calendar.HOUR));
221                cal.set(Calendar.MINUTE, cal.getMaximum(Calendar.MINUTE));
222                cal.set(Calendar.SECOND, cal.getMaximum(Calendar.SECOND));
223    
224                activeTimestamp = new Timestamp(cal.getTime().getTime());
225            }
226    
227            return activeTimestamp;
228        }
229    
230        /**
231         * Changes from/to dates into the range operators the lookupable dao expects ("..",">" etc) this method modifies
232         * the
233         * passed in map and returns a list containing only the modified fields
234         *
235         * @param searchCriteria - map of criteria currently set for which the date criteria will be adjusted
236         */
237        public static Map<String, String> preprocessDateFields(Map<String, String> searchCriteria) {
238            Map<String, String> fieldsToUpdate = new HashMap<String, String>();
239            Set<String> fieldsForLookup = searchCriteria.keySet();
240            for (String propName : fieldsForLookup) {
241                if (propName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
242                    String from_DateValue = searchCriteria.get(propName);
243                    String dateFieldName =
244                            StringUtils.remove(propName, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX);
245                    String to_DateValue = searchCriteria.get(dateFieldName);
246                    String newPropValue = to_DateValue;// maybe clean above with
247                    // ObjectUtils.clean(propertyValue)
248                    if (StringUtils.isNotEmpty(from_DateValue) && StringUtils.isNotEmpty(to_DateValue)) {
249                        newPropValue = from_DateValue + SearchOperator.BETWEEN + to_DateValue;
250                    } else if (StringUtils.isNotEmpty(from_DateValue) && StringUtils.isEmpty(to_DateValue)) {
251                        newPropValue = SearchOperator.GREATER_THAN_EQUAL.op() + from_DateValue;
252                    } else if (StringUtils.isNotEmpty(to_DateValue) && StringUtils.isEmpty(from_DateValue)) {
253                        newPropValue = SearchOperator.LESS_THAN_EQUAL.op() + to_DateValue;
254                    } // could optionally continue on else here
255    
256                    fieldsToUpdate.put(dateFieldName, newPropValue);
257                }
258            }
259    
260            // update lookup values from found date values to update
261            Set<String> keysToUpdate = fieldsToUpdate.keySet();
262            for (String updateKey : keysToUpdate) {
263                searchCriteria.put(updateKey, fieldsToUpdate.get(updateKey));
264            }
265    
266            return fieldsToUpdate;
267        }
268    
269        /**
270         * Checks whether any of the fieldValues being passed refer to a property within an ExternalizableBusinessObject.
271         */
272        public static boolean hasExternalBusinessObjectProperty(Class<?> boClass,
273                Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
274            Object sampleBo = boClass.newInstance();
275            for (String key : fieldValues.keySet()) {
276                if (isExternalBusinessObjectProperty(sampleBo, key)) {
277                    return true;
278                }
279            }
280    
281            return false;
282        }
283    
284        /**
285         * Check whether the given property represents a property within an EBO starting with the sampleBo object given.
286         * This is used to determine if a criteria needs to be applied to the EBO first,
287         * before sending to the normal lookup DAO.
288         */
289        public static boolean isExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
290            if (propertyName.indexOf(".") > 0 && !StringUtils.contains(propertyName, "add.")) {
291                Class<?> propertyClass =
292                        ObjectPropertyUtils.getPropertyType(sampleBo, StringUtils.substringBeforeLast(propertyName, "."));
293                if (propertyClass != null) {
294                    return ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface(propertyClass);
295                }
296            }
297    
298            return false;
299        }
300    
301        /**
302         * Returns a map stripped of any properties which refer to ExternalizableBusinessObjects. These values may not be
303         * passed into the lookup service, since the objects they refer to are not in the
304         * local database.
305         */
306        public static Map<String, String> removeExternalizableBusinessObjectFieldValues(Class<?> boClass,
307                Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
308            Map<String, String> eboFieldValues = new HashMap<String, String>();
309            Object sampleBo = boClass.newInstance();
310            for (String key : fieldValues.keySet()) {
311                if (!isExternalBusinessObjectProperty(sampleBo, key)) {
312                    eboFieldValues.put(key, fieldValues.get(key));
313                }
314            }
315    
316            return eboFieldValues;
317        }
318    
319        /**
320         * Return the EBO fieldValue entries explicitly for the given eboPropertyName. (I.e., any properties with the given
321         * property name as a prefix.
322         */
323        public static Map<String, String> getExternalizableBusinessObjectFieldValues(String eboPropertyName,
324                Map<String, String> fieldValues) {
325            Map<String, String> eboFieldValues = new HashMap<String, String>();
326            for (String key : fieldValues.keySet()) {
327                if (key.startsWith(eboPropertyName + ".")) {
328                    eboFieldValues.put(StringUtils.substringAfterLast(key, "."), fieldValues.get(key));
329                }
330            }
331    
332            return eboFieldValues;
333        }
334    
335        /**
336         * Get the complete list of all properties referenced in the fieldValues that are ExternalizableBusinessObjects.
337         *
338         * This is a list of the EBO object references themselves, not of the properties within them.
339         */
340        public static List<String> getExternalizableBusinessObjectProperties(Class<?> boClass,
341                Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
342            Set<String> eboPropertyNames = new HashSet<String>();
343    
344            Object sampleBo = boClass.newInstance();
345            for (String key : fieldValues.keySet()) {
346                if (isExternalBusinessObjectProperty(sampleBo, key)) {
347                    eboPropertyNames.add(StringUtils.substringBeforeLast(key, "."));
348                }
349            }
350    
351            return new ArrayList<String>(eboPropertyNames);
352        }
353    
354        /**
355         * Given an property on the main BO class, return the defined type of the ExternalizableBusinessObject. This will
356         * be used by other code to determine the correct module service to call for the lookup.
357         *
358         * @param boClass
359         * @param propertyName
360         * @return
361         */
362        public static Class<? extends ExternalizableBusinessObject> getExternalizableBusinessObjectClass(Class<?> boClass,
363                String propertyName) throws IllegalAccessException, InstantiationException {
364            return (Class<? extends ExternalizableBusinessObject>) ObjectPropertyUtils
365                    .getPropertyType(boClass.newInstance(), StringUtils.substringBeforeLast(propertyName, "."));
366        }
367    
368    }