001/**
002 * Copyright 2005-2015 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.uif.service.impl;
017
018import java.text.MessageFormat;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.rice.core.api.CoreApiServiceLocator;
028import org.kuali.rice.core.api.config.property.ConfigurationService;
029import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
030import org.kuali.rice.krad.service.LookupService;
031import org.kuali.rice.krad.uif.UifConstants;
032import org.kuali.rice.krad.uif.UifParameters;
033import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
034import org.kuali.rice.krad.uif.field.AttributeQuery;
035import org.kuali.rice.krad.uif.field.AttributeQueryResult;
036import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata;
037import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
038import org.kuali.rice.krad.uif.service.AttributeQueryService;
039import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
040import org.kuali.rice.krad.uif.widget.LocationSuggest;
041import org.kuali.rice.krad.uif.widget.Suggest;
042import org.kuali.rice.krad.util.BeanPropertyComparator;
043
044/**
045 * Implementation of <code>AttributeQueryService</code> that prepares the attribute queries and
046 * delegates to the <code>LookupService</code>
047 *
048 * @author Kuali Rice Team (rice.collab@kuali.org)
049 */
050public class AttributeQueryServiceImpl implements AttributeQueryService {
051    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
052            AttributeQueryServiceImpl.class);
053
054    private LookupService lookupService;
055    private ConfigurationService configurationService;
056
057    /**
058     * {@inheritDoc}
059     */
060    @Override
061    public AttributeQueryResult performFieldSuggestQuery(ViewPostMetadata viewPostMetadata, String fieldId,
062            String fieldTerm, Map<String, String> queryParameters) {
063        AttributeQueryResult queryResult = new AttributeQueryResult();
064
065        ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId);
066        if (inputFieldMetaData == null) {
067            throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
068        }
069
070        Suggest.SuggestPostData suggestPostData = (Suggest.SuggestPostData) inputFieldMetaData.getData(
071                UifConstants.PostMetadata.SUGGEST);
072
073        AttributeQuery suggestQuery = suggestPostData.getSuggestQuery();
074
075        boolean isUppercaseValue = Boolean.TRUE.equals(inputFieldMetaData.getData(
076                UifConstants.PostMetadata.INPUT_FIELD_IS_UPPERCASE));
077
078        // add term as a like criteria
079        Map<String, String> additionalCriteria = new HashMap<String, String>();
080        if (isUppercaseValue) {
081            additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm.toUpperCase() + "*");
082        } else {
083            additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm + "*");
084        }
085
086        // execute suggest query
087        Collection<?> results = null;
088        if (suggestQuery.hasConfiguredMethod()) {
089            Object queryMethodResult = executeAttributeQueryMethod(suggestQuery, queryParameters, true, fieldTerm);
090            if ((queryMethodResult != null) && (queryMethodResult instanceof Collection<?>)) {
091                results = (Collection<?>) queryMethodResult;
092            }
093        } else {
094            results = executeAttributeQueryCriteria(suggestQuery, queryParameters, additionalCriteria, new ArrayList<String>());
095        }
096
097        // build list of suggest data from result records
098        if (results != null) {
099            if (suggestPostData.isReturnFullQueryObject()) {
100                queryResult.setResultData((List<Object>) results);
101            } else {
102                retrievePropertiesOnResults(queryResult, results, suggestPostData);
103            }
104        }
105
106        return queryResult;
107    }
108
109    /**
110     * Instead of returning the full object this method fills in queryResult with data that contain the properties
111     * of each result object, as configured through the fieldSuggest, from the set of results.
112     *
113     * @param queryResult the queryResult to fill in
114     * @param results the set of original results
115     * @param suggestPostData post data for the suggest widget
116     */
117    protected void retrievePropertiesOnResults(AttributeQueryResult queryResult, Collection<?> results,
118            Suggest.SuggestPostData suggestPostData) {
119        List<Object> suggestData = new ArrayList<Object>();
120        for (Object result : results) {
121            if (result == null) {
122                continue;
123            }
124
125            Map<String, String> propMap = new HashMap<String, String>();
126
127            // if result is type string, use as both value and label
128            if (result instanceof String) {
129                propMap.put(UifParameters.VALUE, (String) result);
130                propMap.put(UifParameters.LABEL, (String) result);
131            }
132
133            // value prop
134            String suggestFieldValue = null;
135            if (StringUtils.isNotBlank(suggestPostData.getValuePropertyName())) {
136                suggestFieldValue = ObjectPropertyUtils.getPropertyValueAsText(result,
137                        suggestPostData.getValuePropertyName());
138            } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.VALUE)) {
139                suggestFieldValue = ObjectPropertyUtils.getPropertyValueAsText(result, UifParameters.VALUE);
140            }
141
142            if (suggestFieldValue != null) {
143                propMap.put(UifParameters.VALUE, suggestFieldValue);
144            }
145
146            // label prop
147            String suggestFieldLabel = null;
148            if (StringUtils.isNotBlank(suggestPostData.getLabelPropertyName())) {
149                suggestFieldLabel = ObjectPropertyUtils.getPropertyValueAsText(result,
150                        suggestPostData.getLabelPropertyName());
151            } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.LABEL)) {
152                suggestFieldLabel = ObjectPropertyUtils.getPropertyValueAsText(result, UifParameters.LABEL);
153            }
154
155            if (suggestFieldLabel != null) {
156                propMap.put(UifParameters.LABEL, suggestFieldLabel);
157            }
158
159            // location suggest specific properties
160            if (suggestPostData instanceof LocationSuggest.LocationSuggestPostData) {
161                handleLocationSuggestProperties((LocationSuggest.LocationSuggestPostData) suggestPostData, result,
162                        propMap);
163            }
164
165            // additional properties
166            handleAdditionalSuggestProperties(suggestPostData, result, propMap);
167
168            // only add if there was a property to send back
169            if (!propMap.isEmpty()) {
170                //TODO: need to apply formatter for field or have method in object property utils
171                suggestData.add(propMap);
172            }
173        }
174
175        queryResult.setResultData(suggestData);
176    }
177
178    /**
179     * Handle the custom additionalProperties set back for a suggestion query.  These will be added to the propMap.
180     *
181     * @param suggestPostData post data for the suggest widget
182     * @param result the result to pull properties from
183     * @param propMap the propMap to add properties to
184     */
185    private void handleAdditionalSuggestProperties(Suggest.SuggestPostData suggestPostData, Object result,
186            Map<String, String> propMap) {
187        if (suggestPostData.getAdditionalPropertiesToReturn() != null) {
188            //add properties for each valid property name
189            for (String propName : suggestPostData.getAdditionalPropertiesToReturn()) {
190                String propValue = null;
191
192                if (StringUtils.isNotBlank(propName) && ObjectPropertyUtils.isReadableProperty(result, propName)) {
193                    propValue = ObjectPropertyUtils.getPropertyValueAsText(result, propName);
194                }
195
196                if (propValue != null) {
197                    propMap.put(propName, propValue);
198                }
199            }
200        }
201    }
202
203    /**
204     * Handle the LocationSuggest specific properties and add them to the map.
205     *
206     * @param suggestPostData post data for the suggest widget
207     * @param result the result to pull properties from
208     * @param propMap the propMap to add properties to
209     */
210    private void handleLocationSuggestProperties(LocationSuggest.LocationSuggestPostData suggestPostData, Object result,
211            Map<String, String> propMap) {
212        // href property
213        String suggestHrefValue = null;
214        if (StringUtils.isNotBlank(suggestPostData.getHrefPropertyName()) && ObjectPropertyUtils.isReadableProperty(
215                result, suggestPostData.getHrefPropertyName())) {
216            suggestHrefValue = ObjectPropertyUtils.getPropertyValueAsText(result,
217                    suggestPostData.getHrefPropertyName());
218        }
219
220        // add if found
221        if (suggestHrefValue != null) {
222            propMap.put(suggestPostData.getHrefPropertyName(), suggestHrefValue);
223        }
224
225        // url addition/appendage property
226        String addUrlValue = null;
227        if (StringUtils.isNotBlank(suggestPostData.getAdditionalUrlPathPropertyName()) &&
228                ObjectPropertyUtils.isReadableProperty(result, suggestPostData.getAdditionalUrlPathPropertyName())) {
229            addUrlValue = ObjectPropertyUtils.getPropertyValueAsText(result,
230                    suggestPostData.getAdditionalUrlPathPropertyName());
231        }
232
233        // add if found
234        if (addUrlValue != null) {
235            propMap.put(suggestPostData.getAdditionalUrlPathPropertyName(), addUrlValue);
236        }
237
238        if (suggestPostData.getRequestParameterPropertyNames() == null) {
239            return;
240        }
241
242        // add properties for each valid requestParameter property name
243        for (String key : suggestPostData.getRequestParameterPropertyNames().keySet()) {
244            String prop = suggestPostData.getRequestParameterPropertyNames().get(key);
245            String propValue = null;
246
247            if (StringUtils.isNotBlank(prop) && ObjectPropertyUtils.isReadableProperty(result, prop)) {
248                propValue = ObjectPropertyUtils.getPropertyValueAsText(result, prop);
249            }
250
251            if (propValue != null) {
252                propMap.put(prop, propValue);
253            }
254        }
255    }
256
257    /**
258     * {@inheritDoc}
259     */
260    @Override
261    public AttributeQueryResult performFieldQuery(ViewPostMetadata viewPostMetadata, String fieldId,
262            Map<String, String> queryParameters) {
263        AttributeQueryResult queryResult = new AttributeQueryResult();
264
265        // retrieve attribute field from view index
266        ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId);
267        if (inputFieldMetaData == null) {
268            throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
269        }
270
271        AttributeQuery fieldQuery = (AttributeQuery) inputFieldMetaData
272                .getData(UifConstants.PostMetadata.INPUT_FIELD_ATTRIBUTE_QUERY);
273        if (fieldQuery == null) {
274            throw new RuntimeException("Field query not defined for field instance with id: " + fieldId);
275        }
276
277        // execute query and get result
278        Object resultObject = null;
279        if (fieldQuery.hasConfiguredMethod()) {
280            Object queryMethodResult = executeAttributeQueryMethod(fieldQuery, queryParameters, false, null);
281            if (queryMethodResult != null) {
282                // if method returned the result then no further processing needed
283                if (queryMethodResult instanceof AttributeQueryResult) {
284                    return (AttributeQueryResult) queryMethodResult;
285                }
286
287                // if method returned collection, take first record
288                if (queryMethodResult instanceof Collection<?>) {
289                    Collection<?> methodResultCollection = (Collection<?>) queryMethodResult;
290                    if (!methodResultCollection.isEmpty()) {
291                        resultObject = methodResultCollection.iterator().next();
292                    }
293                } else {
294                    resultObject = queryMethodResult;
295                }
296            }
297        } else {
298            // execute field query as object lookup
299            Collection<?> results = executeAttributeQueryCriteria(fieldQuery, queryParameters, null,
300                    new ArrayList<String>(queryParameters.keySet()));
301
302            if ((results != null) && !results.isEmpty()) {
303                // expect only one returned row for field query
304                if (results.size() > 1) {
305                    //finding too many results in a not found message (not specific enough)
306                    resultObject = null;
307                } else {
308                    resultObject = results.iterator().next();
309                }
310            }
311        }
312
313        if (resultObject != null) {
314            // build result field data map
315            Map<String, String> resultFieldData = new HashMap<String, String>();
316            for (String fromField : fieldQuery.getReturnFieldMapping().keySet()) {
317                String returnField = fieldQuery.getReturnFieldMapping().get(fromField);
318
319                String fieldValueStr = "";
320                fieldValueStr = ObjectPropertyUtils.getPropertyValueAsText(resultObject, fromField);
321
322                resultFieldData.put(returnField, fieldValueStr);
323            }
324            queryResult.setResultFieldData(resultFieldData);
325
326            fieldQuery.setReturnMessageText("");
327        } else {
328            // add data not found message
329            if (fieldQuery.isRenderNotFoundMessage()) {
330                String messageTemplate = getConfigurationService().getPropertyValueAsString(
331                        UifConstants.MessageKeys.QUERY_DATA_NOT_FOUND);
332                String message = MessageFormat.format(messageTemplate, inputFieldMetaData.getData(
333                        UifConstants.PostMetadata.LABEL));
334                fieldQuery.setReturnMessageText(message.toLowerCase());
335            }
336        }
337
338        // set message and message style classes on query result
339        queryResult.setResultMessage(fieldQuery.getReturnMessageText());
340        queryResult.setResultMessageStyleClasses(fieldQuery.getReturnMessageStyleClasses());
341
342        return queryResult;
343    }
344
345    /**
346     * Prepares the method configured on the attribute query then performs the method invocation
347     *
348     * @param attributeQuery attribute query instance to execute
349     * @param queryParameters map of query parameters that provide values for the method arguments
350     * @param isSuggestQuery indicates whether the query is for forming suggest options
351     * @param queryTerm if being called for a suggest, the term for the query field
352     * @return type depends on method being invoked, could be AttributeQueryResult in which
353     * case the method has prepared the return result, or an Object that needs to be parsed for the result
354     */
355    protected Object executeAttributeQueryMethod(AttributeQuery attributeQuery, Map<String, String> queryParameters,
356            boolean isSuggestQuery, String queryTerm) {
357        String queryMethodToCall = attributeQuery.getQueryMethodToCall();
358        MethodInvokerConfig queryMethodInvoker = attributeQuery.getQueryMethodInvokerConfig();
359
360        if (queryMethodInvoker == null) {
361            queryMethodInvoker = new MethodInvokerConfig();
362        }
363
364        // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
365        // there is not a getter), if so it will override the target method in prepare
366        if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
367            queryMethodInvoker.setTargetMethod(queryMethodToCall);
368        }
369
370        // setup query method arguments
371        List<Object> arguments = new ArrayList<Object>();
372        if ((attributeQuery.getQueryMethodArgumentFieldList() != null) &&
373                (!attributeQuery.getQueryMethodArgumentFieldList().isEmpty())) {
374            // retrieve argument types for conversion and verify method arguments
375            int numQueryMethodArguments = attributeQuery.getQueryMethodArgumentFieldList().size();
376            if (isSuggestQuery) {
377                numQueryMethodArguments += 1;
378            }
379
380            // Empty arguments used to handle overloaded method case
381            queryMethodInvoker.setArguments(new Object[numQueryMethodArguments]);
382            Class<?>[] argumentTypes = queryMethodInvoker.getArgumentTypes();
383
384            if ((argumentTypes == null) || (argumentTypes.length != numQueryMethodArguments)) {
385                throw new RuntimeException(
386                        "Query method argument field list size does not match found number of method arguments");
387            }
388
389            for (int i = 0; i < attributeQuery.getQueryMethodArgumentFieldList().size(); i++) {
390                String methodArgumentFromField = attributeQuery.getQueryMethodArgumentFieldList().get(i);
391                if (queryParameters.containsKey(methodArgumentFromField)) {
392                    arguments.add(queryParameters.get(methodArgumentFromField));
393                } else {
394                    arguments.add(null);
395                }
396            }
397        }
398
399        if (isSuggestQuery) {
400            arguments.add(queryTerm);
401        }
402
403        queryMethodInvoker.setArguments(arguments.toArray());
404
405        try {
406            queryMethodInvoker.prepare();
407
408            return queryMethodInvoker.invoke();
409        } catch (Exception e) {
410            throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
411        }
412    }
413
414    /**
415     * Prepares a query using the configured data object, parameters, and criteria, then executes
416     * the query and returns the result Collection
417     *
418     * @param attributeQuery attribute query instance to perform query for
419     * @param queryParameters map of parameters that will be used in the query criteria
420     * @param additionalCriteria map of additional name/value pairs to add to the critiera
421     * @param wildcardAsLiteralPropertyNames - List of property names with wildcards disabled
422     * @return results of query
423     */
424    protected Collection<?> executeAttributeQueryCriteria(AttributeQuery attributeQuery,
425            Map<String, String> queryParameters, Map<String, String> additionalCriteria,
426            List<String> wildcardAsLiteralPropertyNames) {
427        // build criteria for query
428        boolean allQueryFieldsPresent = true;
429
430        Map<String, String> queryCriteria = new HashMap<String, String>();
431        for (String fieldName : attributeQuery.getQueryFieldMapping().values()) {
432            if (queryParameters.containsKey(fieldName) && StringUtils.isNotBlank(queryParameters.get(fieldName))) {
433                queryCriteria.put(fieldName, queryParameters.get(fieldName));
434            } else {
435                allQueryFieldsPresent = false;
436                break;
437            }
438        }
439
440        // for a field query we need all the criteria
441        if (!allQueryFieldsPresent) {
442            attributeQuery.setRenderNotFoundMessage(false);
443
444            return null;
445        }
446
447        // add any static criteria
448        for (String fieldName : attributeQuery.getAdditionalCriteria().keySet()) {
449            queryCriteria.put(fieldName, attributeQuery.getAdditionalCriteria().get(fieldName));
450        }
451
452        // add additional criteria
453        if (additionalCriteria != null) {
454            queryCriteria.putAll(additionalCriteria);
455        }
456
457        Class<?> queryClass;
458        try {
459            queryClass = Class.forName(attributeQuery.getDataObjectClassName());
460        } catch (ClassNotFoundException e) {
461            throw new RuntimeException(
462                    "Invalid data object class given for suggest query: " + attributeQuery.getDataObjectClassName(), e);
463        }
464
465        // run query
466        Collection<?> results = getLookupService().findCollectionBySearchHelper(queryClass, queryCriteria,
467                wildcardAsLiteralPropertyNames, true, null);
468
469        // sort results
470        if (!attributeQuery.getSortPropertyNames().isEmpty() && (results != null) && (results.size() > 1)) {
471            Collections.sort((List<?>) results, new BeanPropertyComparator(attributeQuery.getSortPropertyNames()));
472        }
473
474        return results;
475    }
476
477    /**
478     * Gets the lookup service
479     *
480     * @return LookupService lookup service
481     */
482    protected LookupService getLookupService() {
483        if (lookupService == null) {
484            lookupService = KRADServiceLocatorWeb.getLookupService();
485        }
486
487        return lookupService;
488    }
489
490    /**
491     * Sets the lookup service
492     *
493     * @param lookupService
494     */
495    public void setLookupService(LookupService lookupService) {
496        this.lookupService = lookupService;
497    }
498
499    /**
500     * Gets the configuration service
501     *
502     * @return configuration service
503     */
504    protected ConfigurationService getConfigurationService() {
505        if (configurationService == null) {
506            configurationService = CoreApiServiceLocator.getKualiConfigurationService();
507        }
508
509        return configurationService;
510    }
511
512    /**
513     * Sets the configuration service
514     *
515     * @param configurationService
516     */
517    public void setConfigurationService(ConfigurationService configurationService) {
518        this.configurationService = configurationService;
519    }
520}