View Javadoc

1   /**
2    * Copyright 2005-2013 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 org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.CoreApiServiceLocator;
20  import org.kuali.rice.core.api.config.property.ConfigurationService;
21  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
22  import org.kuali.rice.krad.service.LookupService;
23  import org.kuali.rice.krad.uif.UifConstants;
24  import org.kuali.rice.krad.uif.UifParameters;
25  import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
26  import org.kuali.rice.krad.uif.field.AttributeQuery;
27  import org.kuali.rice.krad.uif.field.AttributeQueryResult;
28  import org.kuali.rice.krad.uif.field.InputField;
29  import org.kuali.rice.krad.uif.service.AttributeQueryService;
30  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
31  import org.kuali.rice.krad.uif.view.View;
32  import org.kuali.rice.krad.uif.widget.LocationSuggest;
33  import org.kuali.rice.krad.uif.widget.Suggest;
34  import org.kuali.rice.krad.util.BeanPropertyComparator;
35  
36  import java.text.MessageFormat;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
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       * @see org.kuali.rice.krad.uif.service.AttributeQueryService#performFieldSuggestQuery(
59       *org.kuali.rice.krad.uif.view.View, java.lang.String, java.lang.String, java.util.Map<java.lang.String,
60       *      java.lang.String>)
61       */
62      @Override
63      public AttributeQueryResult performFieldSuggestQuery(View view, String fieldId, String fieldTerm,
64              Map<String, String> queryParameters) {
65          AttributeQueryResult queryResult = new AttributeQueryResult();
66  
67          // retrieve attribute field from view index
68          InputField inputField = (InputField) view.getViewIndex().getComponentById(fieldId);
69          if (inputField == null) {
70              throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
71          }
72  
73          Suggest fieldSuggest = inputField.getSuggest();
74          AttributeQuery suggestQuery = fieldSuggest.getSuggestQuery();
75  
76          // add term as a like criteria
77          Map<String, String> additionalCriteria = new HashMap<String, String>();
78          additionalCriteria.put(fieldSuggest.getValuePropertyName(), fieldTerm + "*");
79  
80          // execute suggest query
81          Collection<?> results = null;
82          if (suggestQuery.hasConfiguredMethod()) {
83              Object queryMethodResult = executeAttributeQueryMethod(view, suggestQuery, queryParameters, true,
84                      fieldTerm);
85              if ((queryMethodResult != null) && (queryMethodResult instanceof Collection<?>)) {
86                  results = (Collection<?>) queryMethodResult;
87              }
88          } else {
89              results = executeAttributeQueryCriteria(suggestQuery, queryParameters, additionalCriteria);
90          }
91  
92          // build list of suggest data from result records
93          if (results != null) {
94              if (fieldSuggest.isReturnFullQueryObject()) {
95                  queryResult.setResultData((List<Object>) results);
96              } else {
97                  retrievePropertiesOnResults(queryResult, results, fieldSuggest);
98              }
99          }
100 
101         return queryResult;
102     }
103 
104     /**
105      * Instead of returning the full object this method fills in queryResult with data that contain the properties
106      * of each result object, as configured through the fieldSuggest, from the set of results.
107      *
108      * @param queryResult the queryResult to fill in
109      * @param results the set of original results
110      * @param fieldSuggest the Suggest widget
111      */
112     protected void retrievePropertiesOnResults(AttributeQueryResult queryResult, Collection<?> results,
113             Suggest fieldSuggest) {
114         List<Object> suggestData = new ArrayList<Object>();
115         for (Object result : results) {
116             if (result == null) {
117                 continue;
118             }
119 
120             Map<String, String> propMap = new HashMap<String, String>();
121 
122             // if result is type string, use as both value and label
123             if (result instanceof String) {
124                 propMap.put(UifParameters.VALUE, (String) result);
125                 propMap.put(UifParameters.LABEL, (String) result);
126             }
127 
128             // value prop
129             Object suggestFieldValue = null;
130             if (StringUtils.isNotBlank(fieldSuggest.getValuePropertyName())) {
131                 suggestFieldValue = ObjectPropertyUtils.getPropertyValue(result, fieldSuggest.getValuePropertyName());
132             } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.VALUE)) {
133                 suggestFieldValue = ObjectPropertyUtils.getPropertyValue(result, UifParameters.VALUE);
134             }
135 
136             if (suggestFieldValue != null) {
137                 propMap.put(UifParameters.VALUE, suggestFieldValue.toString());
138             }
139 
140             // label prop
141             Object suggestFieldLabel = null;
142             if (StringUtils.isNotBlank(fieldSuggest.getLabelPropertyName())) {
143                 suggestFieldLabel = ObjectPropertyUtils.getPropertyValue(result, fieldSuggest.getLabelPropertyName());
144             } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.LABEL)) {
145                 suggestFieldLabel = ObjectPropertyUtils.getPropertyValue(result, UifParameters.LABEL);
146             }
147 
148             if (suggestFieldLabel != null) {
149                 propMap.put(UifParameters.LABEL, suggestFieldLabel.toString());
150             }
151 
152             // location suggest specific properties
153             if (fieldSuggest instanceof LocationSuggest) {
154                 handleLocationSuggestProperties((LocationSuggest) fieldSuggest, result, propMap);
155             }
156 
157             // additional properties
158             handleAdditionalSuggestProperties(fieldSuggest, result, propMap);
159 
160             // only add if there was a property to send back
161             if (!propMap.isEmpty()) {
162                 //TODO: need to apply formatter for field or have method in object property utils
163                 suggestData.add(propMap);
164             }
165         }
166 
167         queryResult.setResultData(suggestData);
168     }
169 
170     /**
171      * Handle the custom additionalProperties set back for a suggestion query.  These will be added to the propMap.
172      *
173      * @param fieldSuggest the suggest
174      * @param result the result to pull properties from
175      * @param propMap the propMap to add properties to
176      */
177     private void handleAdditionalSuggestProperties(Suggest fieldSuggest, Object result, Map<String, String> propMap){
178         if (fieldSuggest.getAdditionalPropertiesToReturn() != null){
179             //add properties for each valid property name
180             for(String propName: fieldSuggest.getAdditionalPropertiesToReturn()){
181                 Object propValue = null;
182 
183                 if(StringUtils.isNotBlank(propName)
184                                         && ObjectPropertyUtils.isReadableProperty(result, propName)){
185                     propValue = ObjectPropertyUtils.getPropertyValue(result, propName);
186                 }
187 
188                 if (propValue != null){
189                     propMap.put(propName, propValue.toString());
190                 }
191             }
192         }
193     }
194 
195     /**
196      * Handle the LocationSuggest specific properties and add them to the map.
197      *
198      * @param fieldSuggest the suggest
199      * @param result the result to pull properties from
200      * @param propMap the propMap to add properties to
201      */
202     private void handleLocationSuggestProperties(LocationSuggest fieldSuggest, Object result, Map<String, String> propMap){
203 
204         // href property
205         Object suggestHrefValue = null;
206         if(StringUtils.isNotBlank(fieldSuggest.getHrefPropertyName())
207                 && ObjectPropertyUtils.isReadableProperty(result, fieldSuggest.getHrefPropertyName())){
208             suggestHrefValue = ObjectPropertyUtils.getPropertyValue(result, fieldSuggest.getHrefPropertyName());
209         }
210 
211         // add if found
212         if(suggestHrefValue != null){
213             propMap.put(fieldSuggest.getHrefPropertyName(), suggestHrefValue.toString());
214         }
215 
216         // url addition/appendage property
217         Object addUrlValue = null;
218         if(StringUtils.isNotBlank(fieldSuggest.getAdditionalUrlPathPropertyName())
219                         && ObjectPropertyUtils.isReadableProperty(result, fieldSuggest.getAdditionalUrlPathPropertyName())){
220             addUrlValue = ObjectPropertyUtils.getPropertyValue(result, fieldSuggest.getAdditionalUrlPathPropertyName());
221         }
222 
223         // add if found
224         if(addUrlValue != null){
225             propMap.put(fieldSuggest.getAdditionalUrlPathPropertyName(), addUrlValue.toString());
226         }
227 
228         if(fieldSuggest.getRequestParameterPropertyNames() == null){
229             return;
230         }
231 
232         // add properties for each valid requestParameter property name
233         for(String key: fieldSuggest.getRequestParameterPropertyNames().keySet()){
234             String prop = fieldSuggest.getRequestParameterPropertyNames().get(key);
235             Object propValue = null;
236 
237             if(StringUtils.isNotBlank(prop)
238                                     && ObjectPropertyUtils.isReadableProperty(result, prop)){
239                 propValue = ObjectPropertyUtils.getPropertyValue(result, prop);
240             }
241 
242             if (propValue != null){
243                 propMap.put(prop, propValue.toString());
244             }
245         }
246 
247     }
248 
249     /**
250      * @see org.kuali.rice.krad.uif.service.AttributeQueryService#performFieldQuery(org.kuali.rice.krad.uif.view.View,
251      *      java.lang.String, java.util.Map<java.lang.String,java.lang.String>)
252      */
253     @Override
254     public AttributeQueryResult performFieldQuery(View view, String fieldId, Map<String, String> queryParameters) {
255         AttributeQueryResult queryResult = new AttributeQueryResult();
256 
257         // retrieve attribute field from view index
258         InputField inputField = (InputField) view.getViewIndex().getComponentById(fieldId);
259         if (inputField == null) {
260             throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
261         }
262 
263         AttributeQuery fieldQuery = inputField.getAttributeQuery();
264         if (fieldQuery == null) {
265             throw new RuntimeException("Field query not defined for field instance with id: " + fieldId);
266         }
267 
268         // execute query and get result
269         Object resultObject = null;
270         if (fieldQuery.hasConfiguredMethod()) {
271             Object queryMethodResult = executeAttributeQueryMethod(view, fieldQuery, queryParameters, false, null);
272             if (queryMethodResult != null) {
273                 // if method returned the result then no further processing needed
274                 if (queryMethodResult instanceof AttributeQueryResult) {
275                     return (AttributeQueryResult) queryMethodResult;
276                 }
277 
278                 // if method returned collection, take first record
279                 if (queryMethodResult instanceof Collection<?>) {
280                     Collection<?> methodResultCollection = (Collection<?>) queryMethodResult;
281                     if (!methodResultCollection.isEmpty()) {
282                         resultObject = methodResultCollection.iterator().next();
283                     }
284                 } else {
285                     resultObject = queryMethodResult;
286                 }
287             }
288         } else {
289             // execute field query as object lookup
290             Collection<?> results = executeAttributeQueryCriteria(fieldQuery, queryParameters, null);
291 
292             if ((results != null) && !results.isEmpty()) {
293                 // expect only one returned row for field query
294                 if (results.size() > 1) {
295                     //finding too many results in a not found message (not specific enough)
296                     resultObject = null;
297                 } else {
298                     resultObject = results.iterator().next();
299                 }
300             }
301         }
302 
303         if (resultObject != null) {
304             // build result field data map
305             Map<String, String> resultFieldData = new HashMap<String, String>();
306             for (String fromField : fieldQuery.getReturnFieldMapping().keySet()) {
307                 String returnField = fieldQuery.getReturnFieldMapping().get(fromField);
308 
309                 String fieldValueStr = "";
310                 Object fieldValue = ObjectPropertyUtils.getPropertyValue(resultObject, fromField);
311                 if (fieldValue != null) {
312                     fieldValueStr = fieldValue.toString();
313                 }
314                 resultFieldData.put(returnField, fieldValueStr);
315             }
316             queryResult.setResultFieldData(resultFieldData);
317 
318             fieldQuery.setReturnMessageText("");
319         } else {
320             // add data not found message
321             if (fieldQuery.isRenderNotFoundMessage()) {
322                 String messageTemplate = getConfigurationService().getPropertyValueAsString(
323                         UifConstants.MessageKeys.QUERY_DATA_NOT_FOUND);
324                 String message = MessageFormat.format(messageTemplate, inputField.getLabel());
325                 fieldQuery.setReturnMessageText(message.toLowerCase());
326             }
327         }
328 
329         // set message and message style classes on query result
330         queryResult.setResultMessage(fieldQuery.getReturnMessageText());
331         queryResult.setResultMessageStyleClasses(fieldQuery.getReturnMessageStyleClasses());
332 
333         return queryResult;
334     }
335 
336     /**
337      * Prepares the method configured on the attribute query then performs the method invocation
338      *
339      * @param view view instance the field is contained within
340      * @param attributeQuery attribute query instance to execute
341      * @param queryParameters map of query parameters that provide values for the method arguments
342      * @param isSuggestQuery indicates whether the query is for forming suggest options
343      * @param queryTerm if being called for a suggest, the term for the query field
344      * @return type depends on method being invoked, could be AttributeQueryResult in which
345      *         case the method has prepared the return result, or an Object that needs to be parsed for the result
346      */
347     protected Object executeAttributeQueryMethod(View view, AttributeQuery attributeQuery,
348             Map<String, String> queryParameters, boolean isSuggestQuery, String queryTerm) {
349         String queryMethodToCall = attributeQuery.getQueryMethodToCall();
350         MethodInvokerConfig queryMethodInvoker = attributeQuery.getQueryMethodInvokerConfig();
351 
352         if (queryMethodInvoker == null) {
353             queryMethodInvoker = new MethodInvokerConfig();
354         }
355 
356         // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
357         // there is not a getter), if so it will override the target method in prepare
358         if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
359             queryMethodInvoker.setTargetMethod(queryMethodToCall);
360         }
361 
362         // if target class or object not set, use view helper service
363         if ((queryMethodInvoker.getTargetClass() == null) && (queryMethodInvoker.getTargetObject() == null)) {
364             queryMethodInvoker.setTargetObject(view.getViewHelperService());
365         }
366 
367         // setup query method arguments
368         List<Object> arguments = new ArrayList<Object>();
369         if ((attributeQuery.getQueryMethodArgumentFieldList() != null) && (!attributeQuery
370                 .getQueryMethodArgumentFieldList().isEmpty())) {
371             // retrieve argument types for conversion and verify method arguments
372             int numQueryMethodArguments = attributeQuery.getQueryMethodArgumentFieldList().size();
373             if (isSuggestQuery) {
374                 numQueryMethodArguments += 1;
375             }
376 
377             Class[] argumentTypes = queryMethodInvoker.getArgumentTypes();
378             if ((argumentTypes == null) || (argumentTypes.length != numQueryMethodArguments)) {
379                 throw new RuntimeException(
380                         "Query method argument field list size does not match found number of method arguments");
381             }
382 
383             for (int i = 0; i < attributeQuery.getQueryMethodArgumentFieldList().size(); i++) {
384                 String methodArgumentFromField = attributeQuery.getQueryMethodArgumentFieldList().get(i);
385                 if (queryParameters.containsKey(methodArgumentFromField)) {
386                     arguments.add(queryParameters.get(methodArgumentFromField));
387                 } else {
388                     arguments.add(null);
389                 }
390             }
391         }
392 
393         if (isSuggestQuery) {
394             arguments.add(queryTerm);
395         }
396 
397         queryMethodInvoker.setArguments(arguments.toArray());
398 
399         try {
400             queryMethodInvoker.prepare();
401 
402             return queryMethodInvoker.invoke();
403         } catch (Exception e) {
404             throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
405         }
406     }
407 
408     /**
409      * Prepares a query using the configured data object, parameters, and criteria, then executes
410      * the query and returns the result Collection
411      *
412      * @param attributeQuery attribute query instance to perform query for
413      * @param queryParameters map of parameters that will be used in the query criteria
414      * @param additionalCriteria map of additional name/value pairs to add to the critiera
415      * @return results of query
416      */
417     protected Collection<?> executeAttributeQueryCriteria(AttributeQuery attributeQuery,
418             Map<String, String> queryParameters, Map<String, String> additionalCriteria) {
419         Collection<?> results = null;
420 
421         // build criteria for query
422         Map<String, String> queryCriteria = new HashMap<String, String>();
423         for (String fieldName : attributeQuery.getQueryFieldMapping().values()) {
424             if (queryParameters.containsKey(fieldName) && StringUtils.isNotBlank(queryParameters.get(fieldName))) {
425                 queryCriteria.put(fieldName, queryParameters.get(fieldName));
426             }
427         }
428 
429         // add any static criteria
430         for (String fieldName : attributeQuery.getAdditionalCriteria().keySet()) {
431             queryCriteria.put(fieldName, attributeQuery.getAdditionalCriteria().get(fieldName));
432         }
433 
434         // add additional criteria
435         if (additionalCriteria != null) {
436             queryCriteria.putAll(additionalCriteria);
437         }
438 
439         Class<?> queryClass = null;
440         try {
441             queryClass = Class.forName(attributeQuery.getDataObjectClassName());
442         } catch (ClassNotFoundException e) {
443             throw new RuntimeException(
444                     "Invalid data object class given for suggest query: " + attributeQuery.getDataObjectClassName(), e);
445         }
446 
447         // run query
448         results = getLookupService().findCollectionBySearchUnbounded(queryClass, queryCriteria);
449 
450         // sort results
451         if (!attributeQuery.getSortPropertyNames().isEmpty() && (results != null) && (results.size() > 1)) {
452             Collections.sort((List<?>) results, new BeanPropertyComparator(attributeQuery.getSortPropertyNames()));
453         }
454 
455         return results;
456     }
457 
458     /**
459      * Gets the lookup service
460      *
461      * @return LookupService lookup service
462      */
463     protected LookupService getLookupService() {
464         if (lookupService == null) {
465             lookupService = KRADServiceLocatorWeb.getLookupService();
466         }
467 
468         return lookupService;
469     }
470 
471     /**
472      * Sets the lookup service
473      *
474      * @param lookupService
475      */
476     public void setLookupService(LookupService lookupService) {
477         this.lookupService = lookupService;
478     }
479 
480     /**
481      * Gets the configuration service
482      *
483      * @return configuration service
484      */
485     protected ConfigurationService getConfigurationService() {
486         if (configurationService == null) {
487             configurationService = CoreApiServiceLocator.getKualiConfigurationService();
488         }
489 
490         return configurationService;
491     }
492 
493     /**
494      * Sets the configuration service
495      *
496      * @param configurationService
497      */
498     public void setConfigurationService(ConfigurationService configurationService) {
499         this.configurationService = configurationService;
500     }
501 }