View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.service.impl;
17  
18  import java.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.core.api.CoreApiServiceLocator;
28  import org.kuali.rice.core.api.config.property.ConfigurationService;
29  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
30  import org.kuali.rice.krad.service.LookupService;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.UifParameters;
33  import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
34  import org.kuali.rice.krad.uif.field.AttributeQuery;
35  import org.kuali.rice.krad.uif.field.AttributeQueryResult;
36  import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata;
37  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
38  import org.kuali.rice.krad.uif.service.AttributeQueryService;
39  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
40  import org.kuali.rice.krad.uif.widget.LocationSuggest;
41  import org.kuali.rice.krad.uif.widget.Suggest;
42  import org.kuali.rice.krad.util.BeanPropertyComparator;
43  
44  /**
45   * Implementation of <code>AttributeQueryService</code> that prepares the attribute queries and
46   * delegates to the <code>LookupService</code>
47   *
48   * @author Kuali Rice Team (rice.collab@kuali.org)
49   */
50  public class AttributeQueryServiceImpl implements AttributeQueryService {
51      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
52              AttributeQueryServiceImpl.class);
53  
54      private LookupService lookupService;
55      private ConfigurationService configurationService;
56  
57      /**
58       * {@inheritDoc}
59       */
60      @Override
61      public AttributeQueryResult performFieldSuggestQuery(ViewPostMetadata viewPostMetadata, String fieldId,
62              String fieldTerm, Map<String, String> queryParameters) {
63          AttributeQueryResult queryResult = new AttributeQueryResult();
64  
65          ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId);
66          if (inputFieldMetaData == null) {
67              throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
68          }
69  
70          Suggest.SuggestPostData suggestPostData = (Suggest.SuggestPostData) inputFieldMetaData.getData(
71                  UifConstants.PostMetadata.SUGGEST);
72  
73          AttributeQuery suggestQuery = suggestPostData.getSuggestQuery();
74  
75          boolean isUppercaseValue = Boolean.TRUE.equals(inputFieldMetaData.getData(
76                  UifConstants.PostMetadata.INPUT_FIELD_IS_UPPERCASE));
77  
78          // add term as a like criteria
79          Map<String, String> additionalCriteria = new HashMap<String, String>();
80          if (isUppercaseValue) {
81              additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm.toUpperCase() + "*");
82          } else {
83              additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm + "*");
84          }
85  
86          // execute suggest query
87          Collection<?> results = null;
88          if (suggestQuery.hasConfiguredMethod()) {
89              Object queryMethodResult = executeAttributeQueryMethod(suggestQuery, queryParameters, true, fieldTerm);
90              if ((queryMethodResult != null) && (queryMethodResult instanceof Collection<?>)) {
91                  results = (Collection<?>) queryMethodResult;
92              }
93          } else {
94              results = executeAttributeQueryCriteria(suggestQuery, queryParameters, additionalCriteria, new ArrayList<String>());
95          }
96  
97          // build list of suggest data from result records
98          if (results != null) {
99              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             Object suggestFieldValue = null;
135             if (StringUtils.isNotBlank(suggestPostData.getValuePropertyName())) {
136                 suggestFieldValue = ObjectPropertyUtils.getPropertyValue(result,
137                         suggestPostData.getValuePropertyName());
138             } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.VALUE)) {
139                 suggestFieldValue = ObjectPropertyUtils.getPropertyValue(result, UifParameters.VALUE);
140             }
141 
142             if (suggestFieldValue != null) {
143                 propMap.put(UifParameters.VALUE, suggestFieldValue.toString());
144             }
145 
146             // label prop
147             Object suggestFieldLabel = null;
148             if (StringUtils.isNotBlank(suggestPostData.getLabelPropertyName())) {
149                 suggestFieldLabel = ObjectPropertyUtils.getPropertyValue(result,
150                         suggestPostData.getLabelPropertyName());
151             } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.LABEL)) {
152                 suggestFieldLabel = ObjectPropertyUtils.getPropertyValue(result, UifParameters.LABEL);
153             }
154 
155             if (suggestFieldLabel != null) {
156                 propMap.put(UifParameters.LABEL, suggestFieldLabel.toString());
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                 Object propValue = null;
191 
192                 if (StringUtils.isNotBlank(propName) && ObjectPropertyUtils.isReadableProperty(result, propName)) {
193                     propValue = ObjectPropertyUtils.getPropertyValue(result, propName);
194                 }
195 
196                 if (propValue != null) {
197                     propMap.put(propName, propValue.toString());
198                 }
199             }
200         }
201     }
202 
203     /**
204      * Handle the LocationSuggest specific properties and add them to the map.
205      *
206      * @param suggestPostData oost 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         Object suggestHrefValue = null;
214         if (StringUtils.isNotBlank(suggestPostData.getHrefPropertyName()) && ObjectPropertyUtils.isReadableProperty(
215                 result, suggestPostData.getHrefPropertyName())) {
216             suggestHrefValue = ObjectPropertyUtils.getPropertyValue(result, suggestPostData.getHrefPropertyName());
217         }
218 
219         // add if found
220         if (suggestHrefValue != null) {
221             propMap.put(suggestPostData.getHrefPropertyName(), suggestHrefValue.toString());
222         }
223 
224         // url addition/appendage property
225         Object addUrlValue = null;
226         if (StringUtils.isNotBlank(suggestPostData.getAdditionalUrlPathPropertyName()) &&
227                 ObjectPropertyUtils.isReadableProperty(result, suggestPostData.getAdditionalUrlPathPropertyName())) {
228             addUrlValue = ObjectPropertyUtils.getPropertyValue(result,
229                     suggestPostData.getAdditionalUrlPathPropertyName());
230         }
231 
232         // add if found
233         if (addUrlValue != null) {
234             propMap.put(suggestPostData.getAdditionalUrlPathPropertyName(), addUrlValue.toString());
235         }
236 
237         if (suggestPostData.getRequestParameterPropertyNames() == null) {
238             return;
239         }
240 
241         // add properties for each valid requestParameter property name
242         for (String key : suggestPostData.getRequestParameterPropertyNames().keySet()) {
243             String prop = suggestPostData.getRequestParameterPropertyNames().get(key);
244             Object propValue = null;
245 
246             if (StringUtils.isNotBlank(prop) && ObjectPropertyUtils.isReadableProperty(result, prop)) {
247                 propValue = ObjectPropertyUtils.getPropertyValue(result, prop);
248             }
249 
250             if (propValue != null) {
251                 propMap.put(prop, propValue.toString());
252             }
253         }
254     }
255 
256     /**
257      * {@inheritDoc}
258      */
259     @Override
260     public AttributeQueryResult performFieldQuery(ViewPostMetadata viewPostMetadata, String fieldId,
261             Map<String, String> queryParameters) {
262         AttributeQueryResult queryResult = new AttributeQueryResult();
263 
264         // retrieve attribute field from view index
265         ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId);
266         if (inputFieldMetaData == null) {
267             throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
268         }
269 
270         AttributeQuery fieldQuery = (AttributeQuery) inputFieldMetaData
271                 .getData(UifConstants.PostMetadata.INPUT_FIELD_ATTRIBUTE_QUERY);
272         if (fieldQuery == null) {
273             throw new RuntimeException("Field query not defined for field instance with id: " + fieldId);
274         }
275 
276         // execute query and get result
277         Object resultObject = null;
278         if (fieldQuery.hasConfiguredMethod()) {
279             Object queryMethodResult = executeAttributeQueryMethod(fieldQuery, queryParameters, false, null);
280             if (queryMethodResult != null) {
281                 // if method returned the result then no further processing needed
282                 if (queryMethodResult instanceof AttributeQueryResult) {
283                     return (AttributeQueryResult) queryMethodResult;
284                 }
285 
286                 // if method returned collection, take first record
287                 if (queryMethodResult instanceof Collection<?>) {
288                     Collection<?> methodResultCollection = (Collection<?>) queryMethodResult;
289                     if (!methodResultCollection.isEmpty()) {
290                         resultObject = methodResultCollection.iterator().next();
291                     }
292                 } else {
293                     resultObject = queryMethodResult;
294                 }
295             }
296         } else {
297             // execute field query as object lookup
298             Collection<?> results = executeAttributeQueryCriteria(fieldQuery, queryParameters, null,
299                     new ArrayList<String>(queryParameters.keySet()));
300 
301             if ((results != null) && !results.isEmpty()) {
302                 // expect only one returned row for field query
303                 if (results.size() > 1) {
304                     //finding too many results in a not found message (not specific enough)
305                     resultObject = null;
306                 } else {
307                     resultObject = results.iterator().next();
308                 }
309             }
310         }
311 
312         if (resultObject != null) {
313             // build result field data map
314             Map<String, String> resultFieldData = new HashMap<String, String>();
315             for (String fromField : fieldQuery.getReturnFieldMapping().keySet()) {
316                 String returnField = fieldQuery.getReturnFieldMapping().get(fromField);
317 
318                 String fieldValueStr = "";
319                 Object fieldValue = ObjectPropertyUtils.getPropertyValue(resultObject, fromField);
320                 if (fieldValue != null) {
321                     fieldValueStr = fieldValue.toString();
322                 }
323                 resultFieldData.put(returnField, fieldValueStr);
324             }
325             queryResult.setResultFieldData(resultFieldData);
326 
327             fieldQuery.setReturnMessageText("");
328         } else {
329             // add data not found message
330             if (fieldQuery.isRenderNotFoundMessage()) {
331                 String messageTemplate = getConfigurationService().getPropertyValueAsString(
332                         UifConstants.MessageKeys.QUERY_DATA_NOT_FOUND);
333                 String message = MessageFormat.format(messageTemplate, inputFieldMetaData.getData(
334                         UifConstants.PostMetadata.LABEL));
335                 fieldQuery.setReturnMessageText(message.toLowerCase());
336             }
337         }
338 
339         // set message and message style classes on query result
340         queryResult.setResultMessage(fieldQuery.getReturnMessageText());
341         queryResult.setResultMessageStyleClasses(fieldQuery.getReturnMessageStyleClasses());
342 
343         return queryResult;
344     }
345 
346     /**
347      * Prepares the method configured on the attribute query then performs the method invocation
348      *
349      * @param attributeQuery attribute query instance to execute
350      * @param queryParameters map of query parameters that provide values for the method arguments
351      * @param isSuggestQuery indicates whether the query is for forming suggest options
352      * @param queryTerm if being called for a suggest, the term for the query field
353      * @return type depends on method being invoked, could be AttributeQueryResult in which
354      * case the method has prepared the return result, or an Object that needs to be parsed for the result
355      */
356     protected Object executeAttributeQueryMethod(AttributeQuery attributeQuery, Map<String, String> queryParameters,
357             boolean isSuggestQuery, String queryTerm) {
358         String queryMethodToCall = attributeQuery.getQueryMethodToCall();
359         MethodInvokerConfig queryMethodInvoker = attributeQuery.getQueryMethodInvokerConfig();
360 
361         if (queryMethodInvoker == null) {
362             queryMethodInvoker = new MethodInvokerConfig();
363         }
364 
365         // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
366         // there is not a getter), if so it will override the target method in prepare
367         if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
368             queryMethodInvoker.setTargetMethod(queryMethodToCall);
369         }
370 
371         // setup query method arguments
372         List<Object> arguments = new ArrayList<Object>();
373         if ((attributeQuery.getQueryMethodArgumentFieldList() != null) &&
374                 (!attributeQuery.getQueryMethodArgumentFieldList().isEmpty())) {
375             // retrieve argument types for conversion and verify method arguments
376             int numQueryMethodArguments = attributeQuery.getQueryMethodArgumentFieldList().size();
377             if (isSuggestQuery) {
378                 numQueryMethodArguments += 1;
379             }
380 
381             // Empty arguments used to handle overloaded method case
382             queryMethodInvoker.setArguments(new Object[numQueryMethodArguments]);
383             Class<?>[] argumentTypes = queryMethodInvoker.getArgumentTypes();
384 
385             if ((argumentTypes == null) || (argumentTypes.length != numQueryMethodArguments)) {
386                 throw new RuntimeException(
387                         "Query method argument field list size does not match found number of method arguments");
388             }
389 
390             for (int i = 0; i < attributeQuery.getQueryMethodArgumentFieldList().size(); i++) {
391                 String methodArgumentFromField = attributeQuery.getQueryMethodArgumentFieldList().get(i);
392                 if (queryParameters.containsKey(methodArgumentFromField)) {
393                     arguments.add(queryParameters.get(methodArgumentFromField));
394                 } else {
395                     arguments.add(null);
396                 }
397             }
398         }
399 
400         if (isSuggestQuery) {
401             arguments.add(queryTerm);
402         }
403 
404         queryMethodInvoker.setArguments(arguments.toArray());
405 
406         try {
407             queryMethodInvoker.prepare();
408 
409             return queryMethodInvoker.invoke();
410         } catch (Exception e) {
411             throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
412         }
413     }
414 
415     /**
416      * Prepares a query using the configured data object, parameters, and criteria, then executes
417      * the query and returns the result Collection
418      *
419      * @param attributeQuery attribute query instance to perform query for
420      * @param queryParameters map of parameters that will be used in the query criteria
421      * @param additionalCriteria map of additional name/value pairs to add to the critiera
422      * @param wildcardAsLiteralPropertyNames - List of property names with wildcards disabled
423      * @return results of query
424      */
425     protected Collection<?> executeAttributeQueryCriteria(AttributeQuery attributeQuery,
426             Map<String, String> queryParameters, Map<String, String> additionalCriteria,
427             List<String> wildcardAsLiteralPropertyNames) {
428         // build criteria for query
429         boolean allQueryFieldsPresent = true;
430 
431         Map<String, String> queryCriteria = new HashMap<String, String>();
432         for (String fieldName : attributeQuery.getQueryFieldMapping().values()) {
433             if (queryParameters.containsKey(fieldName) && StringUtils.isNotBlank(queryParameters.get(fieldName))) {
434                 queryCriteria.put(fieldName, queryParameters.get(fieldName));
435             } else {
436                 allQueryFieldsPresent = false;
437                 break;
438             }
439         }
440 
441         // for a field query we need all the criteria
442         if (!allQueryFieldsPresent) {
443             attributeQuery.setRenderNotFoundMessage(false);
444 
445             return null;
446         }
447 
448         // add any static criteria
449         for (String fieldName : attributeQuery.getAdditionalCriteria().keySet()) {
450             queryCriteria.put(fieldName, attributeQuery.getAdditionalCriteria().get(fieldName));
451         }
452 
453         // add additional criteria
454         if (additionalCriteria != null) {
455             queryCriteria.putAll(additionalCriteria);
456         }
457 
458         Class<?> queryClass;
459         try {
460             queryClass = Class.forName(attributeQuery.getDataObjectClassName());
461         } catch (ClassNotFoundException e) {
462             throw new RuntimeException(
463                     "Invalid data object class given for suggest query: " + attributeQuery.getDataObjectClassName(), e);
464         }
465 
466         // run query
467         Collection<?> results = getLookupService().findCollectionBySearchHelper(queryClass, queryCriteria,
468                 wildcardAsLiteralPropertyNames, true, null);
469 
470         // sort results
471         if (!attributeQuery.getSortPropertyNames().isEmpty() && (results != null) && (results.size() > 1)) {
472             Collections.sort((List<?>) results, new BeanPropertyComparator(attributeQuery.getSortPropertyNames()));
473         }
474 
475         return results;
476     }
477 
478     /**
479      * Gets the lookup service
480      *
481      * @return LookupService lookup service
482      */
483     protected LookupService getLookupService() {
484         if (lookupService == null) {
485             lookupService = KRADServiceLocatorWeb.getLookupService();
486         }
487 
488         return lookupService;
489     }
490 
491     /**
492      * Sets the lookup service
493      *
494      * @param lookupService
495      */
496     public void setLookupService(LookupService lookupService) {
497         this.lookupService = lookupService;
498     }
499 
500     /**
501      * Gets the configuration service
502      *
503      * @return configuration service
504      */
505     protected ConfigurationService getConfigurationService() {
506         if (configurationService == null) {
507             configurationService = CoreApiServiceLocator.getKualiConfigurationService();
508         }
509 
510         return configurationService;
511     }
512 
513     /**
514      * Sets the configuration service
515      *
516      * @param configurationService
517      */
518     public void setConfigurationService(ConfigurationService configurationService) {
519         this.configurationService = configurationService;
520     }
521 }