001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.lookup;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.ojb.broker.query.Criteria;
020import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.encryption.EncryptionService;
023import org.kuali.rice.core.api.search.SearchOperator;
024import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
025import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
026import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
027import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
028import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
029import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
030import org.kuali.rice.krad.util.KRADConstants;
031import org.kuali.rice.krad.util.KRADPropertyConstants;
032import org.kuali.rice.krad.util.ObjectUtils;
033
034import java.sql.Date;
035import java.sql.Timestamp;
036import java.text.ParseException;
037import java.util.ArrayList;
038import java.util.Calendar;
039import java.util.HashMap;
040import java.util.HashSet;
041import java.util.List;
042import java.util.Map;
043import 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 */
050public 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.KNS_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}